circus-0.12.1/000077500000000000000000000000001256046442300130715ustar00rootroot00000000000000circus-0.12.1/.coveragerc000066400000000000000000000001621256046442300152110ustar00rootroot00000000000000[run] omit = *_patch*,circus/tests/* source = circus include = circus/* parallel = true [html] directory = html circus-0.12.1/.coveralls.sh000077500000000000000000000001111256046442300154710ustar00rootroot00000000000000if [ "$TOX_ENV" = "py27" ] then pip install coveralls coveralls fi circus-0.12.1/.gitignore000066400000000000000000000004471256046442300150660ustar00rootroot00000000000000.DS_Store *.pyc *.pyo circus.egg-info ^bin ^lib ^lib64 ^include .Python .coverage html docs/source/for-ops/commands/ docs/source/for-ops/commands.rst docs/build local examples/test.log man/ .tox/ build/ # ignore patterns for buildout .installed.cfg develop-eggs/* eggs/* parts/* *.un~ *.swp .pc circus-0.12.1/.travis.yml000066400000000000000000000007661256046442300152130ustar00rootroot00000000000000language: python before_install: - sudo apt-get install -y libev-dev - sudo apt-get install -y libevent-dev python: 2.7 env: - TOX_ENV=py32 - TOX_ENV=py33 - TOX_ENV=py34 - TOX_ENV=py26-no-gevent - TOX_ENV=py27-no-gevent - TOX_ENV=docs - TOX_ENV=flake8 - TOX_ENV=circus-web - TOX_ENV=py26 - TOX_ENV=py27 script: - tox -e $TOX_ENV install: - pip install tox notifications: email: tarek@mozilla.com irc: "irc.freenode.org#mozilla-circus" on_success: change circus-0.12.1/CONTRIBUTORS.txt000066400000000000000000000067101256046442300155730ustar00rootroot00000000000000List of contributors: - Tarek Ziadé - tarek@mozilla.com - Benoit Chesneau - benoit@e-engura.com - Pratyk S. Paudel (logo) - Neil Chintomby - nchintomby@gmail.com - Ori Livneh - ori.livneh@gmail.com - Pete Fritchman - petef@mozilla.com - Johan Charpentier - contact@cyberj.me - John Morrison - jmorrison@mozilla.com - Chris McDonald - xwraithanx@gmail.com - Stefane Fermigier - sf@fermigier.com - Adnane Belmadiaf - daker@ubuntu.com - Alexis Métaireau - alexis@mozilla.com - Christian S. Perone - christian.perone@gmail.com - Stephane Wirtel - stephane@wirtel.be - Paul Meserve - paul@pogodan.com - Jamie Matthews - jamie.matthews@gmail.com - Nicholas Pellegrino - npellegrino@mozilla.com - Marc Sibson - @sibson - Benjamin Kampmann - ben.kampmann@gmail.com - Ben Bangert - ben@groovie.org - David Miller - david@deadpansincerity.com - Jean-Michel ARMAND - j-mad@j-mad.com - Kamil Kisiel - kamil@kamilkisiel.net - Kris Beevers - kbeevers@voxel.net - Marc Abramowitz - marc@marc-abramowitz.com - Paul - paul@pogodan.com - Rémy HUBSCHER - hubscher.remy@gmail.com - Richard Newman - rnewman@twinql.com - Roman Imankulov - roman@netangels.ru - Wieland Hoffmann - themineo@gmail.com - Mathieu Agopian - mathieu.agopian@gmail.com - Alvaro Saurin - saurin@tid.es - Austin Morton - amorton@juvsoft.com - Philip Thrasher - philipthrasher@gmail.com - Zohar Zilberman - popen2@gmail.com - Jeroen Dekkers - jeroen@dekkers.ch - Andrews Medina - andrewsmedina@gmail.com - Roberto De Ioris - roberto@unbit.it - Mark Steve Samson - hello@marksteve.com - Boris Feld - lothiraldan@gmail.com - Arek Czechowsk - agend07@gmail.com - Peter Parkanyi - me@rhapsodhy.hu - Sebastian Pawluś - sebastian.pawlus@gmail.com - Xabier Larrakoetxea - slok69@gmail.com - Marc Rijken - marc@rijken.org - Marcus Brinkmann - m.brinkmann@semantics.de - Victor Godoy Poluceno - victorpoluceno@gmail.com - Thomas Chiroux - thomas@chiroux.com - Steven Armstrong - steven-circus@armstrong.cc - Alessandro Molina - Alon Horev - Ben Bangert - Cezar Sa Espinola - Christopher Grebs - Dane Knecht - Devon Meunier - Eric BREHAULT - Eric Larson - Francisco Souza - Horst Gutmann - Jeff Schroeder - John MacKenzie - Julien Tayon - Kevin Le - Laurent Coustet - Lee Begg - Leonardo Santagada - Mark Steve Samson - Matt Long - Michele Lacchia - Nick Pellegrino - Przemysław Suliga - Rachid Belaid - Rohit Sankaran - Shezad Khan - Simon Wachter - Sven Wilhelm - Wieland Hoffmann - Wraithan - Alex Marandon - Fabien Marty - Scott Maxwell - Dong Weiming - Wynn Wilkes - Sylvain Viollon - James Ascroft-Leigh - Mike Dunn - Yannick PEROUX - David Douard circus-0.12.1/LICENSE000066400000000000000000000011231256046442300140730ustar00rootroot00000000000000Copyright 2012 - Mozilla Foundation Copyright 2012 - Benoit Chesneau Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. circus-0.12.1/MANIFEST.in000066400000000000000000000001311256046442300146220ustar00rootroot00000000000000include CONTRIBUTORS.txt include README.rst include LICENSE include pip-requirements.txt circus-0.12.1/Makefile000066400000000000000000000016751256046442300145420ustar00rootroot00000000000000.PHONY: docs build test coverage build_rpm clean ifndef VTENV_OPTS VTENV_OPTS = -p python2.7 --no-site-packages endif VENV?=virtualenv bin/python: $(VENV) $(VTENV_OPTS) . bin/python setup.py develop test: bin/python bin/pip install tox bin/tox docs: bin/pip install -r doc-requirements.txt --use-mirrors SPHINXBUILD=../bin/sphinx-build $(MAKE) -C docs html $^ coverage: bin/coverage rm -f `pwd`/.coverage rm -rf `pwd`/html - COVERAGE_PROCESS_START=`pwd`/.coveragerc COVERAGE_FILE=`pwd`/.coverage PYTHONPATH=`pwd` bin/nosetests -s circus/tests bin/coverage combine bin/coverage html bin/coverage: bin/python bin/pip install -r test-requirements.txt --use-mirrors bin/pip install nose coverage build_rpm: bin/python setup.py bdist_rpm --requires "python26 python-setuptools pyzmq python26-psutil" clean: rm -rf bin .tox include/ lib/ man/ circus.egg-info/ build/ find . -name "*.pyc" | xargs rm -f find . -name "*.un~" | xargs rm -f circus-0.12.1/README.rst000066400000000000000000000021531256046442300145610ustar00rootroot00000000000000====== Circus ====== Circus is a program that runs and watches processes and sockets. Circus can be used as a library or through the command line. .. image:: https://secure.travis-ci.org/circus-tent/circus.svg?branch=master :alt: Build Status :target: https://secure.travis-ci.org/circus-tent/circus .. image:: https://coveralls.io/repos/circus-tent/circus/badge.png?branch=master :alt: Coverage Status on master :target: https://coveralls.io/r/circus-tent/circus?branch=master .. image:: https://img.shields.io/pypi/v/circus.png :target: https://python.org/pypi/circus/ .. image:: https://img.shields.io/pypi/dm/circus.png :target: https://python.org/pypi/circus/ .. image:: http://allmychanges.com/p/python/circus/badge/ :target: http://allmychanges.com/p/python/circus/?utm_source=badge Links: - Full Documentation : http://circus.readthedocs.org - How to Contribute : http://circus.readthedocs.org/en/latest/contributing/ - Mailing List : https://groups.yahoo.com/neo/groups/circus-dev/info - Repository & Issue Tracker : https://github.com/circus-tent/circus - IRC : Freenode, channel #mozilla-circus circus-0.12.1/circus/000077500000000000000000000000001256046442300143615ustar00rootroot00000000000000circus-0.12.1/circus/__init__.py000066400000000000000000000163261256046442300165020ustar00rootroot00000000000000import logging import os import warnings version_info = (0, 12, 1) __version__ = ".".join(map(str, version_info)) # This config call is done to avoid any # "no handlers could be found for logger" # However, the real configuration has to be done later logging.basicConfig() logger = logging.getLogger('circus') class ArbiterHandler(object): def __call__(self, watchers, controller=None, pubsub_endpoint=None, statsd=False, stats_endpoint=None, statsd_close_outputs=False, multicast_endpoint=None, env=None, name=None, context=None, background=False, stream_backend="thread", httpd=False, plugins=None, debug=False, proc_name="circusd", loop=None, check_delay=1.0, **kw): """Creates a Arbiter and a single watcher in it. Options: - **watchers** -- a list of watchers. A watcher in that case is a dict containing: - **name** -- the name of the watcher (default: None) - **cmd** -- the command line used to run the Watcher. - **args** -- the args for the command (list or string). - **executable** -- When executable is given, the first item in the args sequence obtained from **cmd** is still treated by most programs as the command name, which can then be different from the actual executable name. It becomes the display name for the executing program in utilities such as **ps**. - **numprocesses** -- the number of processes to spawn (default: 1). - **warmup_delay** -- the delay in seconds between two spawns (default: 0) - **shell** -- if True, the processes are run in the shell (default: False) - **working_dir** - the working dir for the processes (default: None) - **uid** -- the user id used to run the processes (default: None) - **gid** -- the group id used to run the processes (default: None) - **env** -- the environment passed to the processes (default: None) - **send_hup**: if True, a process reload will be done by sending the SIGHUP signal. (default: False) - **stdout_stream**: a mapping containing the options for configuring the stdout stream. Default to None. When provided, may contain: - **class**: the fully qualified name of the class to use for streaming. Defaults to circus.stream.FileStream - any other key will be passed the class constructor. - **stderr_stream**: a mapping containing the options for configuring the stderr stream. Default to None. When provided, may contain: - **class**: the fully qualified name of the class to use for streaming. Defaults to circus.stream.FileStream - any other key will be passed the class constructor. - **max_retry**: the number of times we attempt to start a process, before we abandon and stop the whole watcher. (default: 5) - **hooks**: callback functions for hooking into the watcher startup and shutdown process. **hooks** is a dict where each key is the hook name and each value is a 2-tuple with the name of the callable or the callabled itself and a boolean flag indicating if an exception occuring in the hook should not be ignored. Possible values for the hook name: *before_start*, *after_start*, *before_spawn*, *after_spawn*, *before_stop*, *after_stop*, *before_signal*, *after_signal*, *extended_stats* - **controller** -- the zmq entry point (default: 'tcp://127.0.0.1:5555') - **pubsub_endpoint** -- the zmq entry point for the pubsub (default: 'tcp://127.0.0.1:5556') - **stats_endpoint** -- the stats endpoint. If not provided, the *circusd-stats* process will not be launched. (default: None) - **statsd_close_outputs** -- if True sends the circusd-stats stdout/stderr to /dev/null (default: False) - **context** -- the zmq context (default: None) - **background** -- If True, the arbiter is launched in a thread in the background (default: False) - **stream_backend** -- the backend that will be used for the streaming process. Can be *thread* or *gevent*. When set to *gevent* you need to have *gevent* and *gevent_zmq* installed. (default: thread) - **plugins** -- a list of plugins. Each item is a mapping with: - **use** -- Fully qualified name that points to the plugin class - every other value is passed to the plugin in the **config** option - **debug** -- If True the arbiter is launched in debug mode (default: False) - **proc_name** -- the arbiter process name (default: circusd) - **loop** -- the event loop (default: None) - **check_delay** -- the delay between two controller points (default: 1 s) """ from circus.util import (DEFAULT_ENDPOINT_DEALER, DEFAULT_ENDPOINT_SUB, DEFAULT_ENDPOINT_MULTICAST, DEFAULT_ENDPOINT_STATS) if controller is None: controller = DEFAULT_ENDPOINT_DEALER if pubsub_endpoint is None: pubsub_endpoint = DEFAULT_ENDPOINT_SUB if multicast_endpoint is None: multicast_endpoint = DEFAULT_ENDPOINT_MULTICAST if stats_endpoint is None and statsd: stats_endpoint = DEFAULT_ENDPOINT_STATS elif stats_endpoint is not None and not statsd: warnings.warn("You defined a stats_endpoint without " "setting up statsd to True.", DeprecationWarning) statsd = True from circus.watcher import Watcher Arbiter = self._get_arbiter_klass(background=background) _watchers = [] for watcher in watchers: cmd = watcher['cmd'] watcher['name'] = watcher.get('name', os.path.basename(cmd.split()[0])) watcher['stream_backend'] = stream_backend _watchers.append(Watcher.load_from_config(watcher)) return Arbiter(_watchers, controller, pubsub_endpoint, httpd=httpd, statsd=statsd, stats_endpoint=stats_endpoint, statsd_close_outputs=statsd_close_outputs, multicast_endpoint=multicast_endpoint, context=context, plugins=plugins, debug=debug, proc_name=proc_name, loop=loop, check_delay=check_delay, **kw) def _get_arbiter_klass(self, background): if background: from circus.arbiter import ThreadedArbiter as Arbiter # NOQA else: from circus.arbiter import Arbiter # NOQA return Arbiter get_arbiter = ArbiterHandler() circus-0.12.1/circus/_patch.py000066400000000000000000000027001256046442300161700ustar00rootroot00000000000000import threading from threading import _active_limbo_lock, _active, _sys from .util import get_python_version debugger = False try: # noinspection PyUnresolvedReferences import pydevd debugger = pydevd.GetGlobalDebugger() except ImportError: pass if not debugger: # see http://bugs.python.org/issue1596321 if hasattr(threading.Thread, '_Thread__delete'): def _delete(self): try: with _active_limbo_lock: del _active[self._Thread__ident] except KeyError: if 'dummy_threading' not in _sys.modules: raise threading.Thread._Thread__delete = _delete else: def _delete(self): # NOQA try: with _active_limbo_lock: del _active[self._ident] except KeyError: if 'dummy_threading' not in _sys.modules: raise threading.Thread._delete = _delete # see http://bugs.python.org/issue14308 if get_python_version() < (2, 7, 0): def _stop(self): # DummyThreads delete self.__block, but they have no waiters to # notify anyway (join() is forbidden on them). if not hasattr(self, '_Thread__block'): return self._Thread__stop_old() threading.Thread._Thread__stop_old = threading.Thread._Thread__stop threading.Thread._Thread__stop = _stop circus-0.12.1/circus/arbiter.py000066400000000000000000000737171256046442300164020ustar00rootroot00000000000000import errno import logging import os import gc from circus.fixed_threading import Thread, get_ident import sys from time import sleep import select import socket from tornado import gen import zmq from zmq.eventloop import ioloop from circus.controller import Controller from circus.exc import AlreadyExist from circus import logger from circus.watcher import Watcher from circus.util import debuglog, _setproctitle, parse_env_dict from circus.util import DictDiffer, synchronized, tornado_sleep, papa from circus.util import IS_WINDOWS from circus.config import get_config from circus.plugins import get_plugin_cmd from circus.sockets import CircusSocket, CircusSockets _ENV_EXCEPTIONS = ('__CF_USER_TEXT_ENCODING', 'PS1', 'COMP_WORDBREAKS', 'PROMPT_COMMAND') class Arbiter(object): """Class used to control a list of watchers. Options: - **watchers** -- a list of Watcher objects - **endpoint** -- the controller ZMQ endpoint - **pubsub_endpoint** -- the pubsub endpoint - **statsd** -- If True, a circusd-stats process is run (default: False) - **stats_endpoint** -- the stats endpoint. - **statsd_close_outputs** -- if True sends the circusd-stats stdout/stderr to /dev/null (default: False) - **multicast_endpoint** -- the multicast endpoint for circusd cluster auto-discovery (default: udp://237.219.251.97:12027) Multicast addr should be between 224.0.0.0 to 239.255.255.255 and the same for the all cluster. - **check_delay** -- the delay between two controller points (default: 1 s) - **prereload_fn** -- callable that will be executed on each reload (default: None) - **context** -- if provided, the zmq context to reuse. (default: None) - **loop**: if provided, a :class:`zmq.eventloop.ioloop.IOLoop` instance to reuse. (default: None) - **plugins** -- a list of plugins. Each item is a mapping with: - **use** -- Fully qualified name that points to the plugin class - every other value is passed to the plugin in the **config** option - **sockets** -- a mapping of sockets. Each key is the socket name, and each value a :class:`CircusSocket` class. (default: None) - **warmup_delay** -- a delay in seconds between two watchers startup. (default: 0) - **httpd** -- If True, a circushttpd process is run (default: False) - **httpd_host** -- the circushttpd host (default: localhost) - **httpd_port** -- the circushttpd port (default: 8080) - **httpd_close_outputs** -- if True, sends circushttpd stdout/stderr to /dev/null. (default: False) - **debug** -- if True, adds a lot of debug info in the stdout (default: False) - **debug_gc** -- if True, does gc.set_debug(gc.DEBUG_LEAK) (default: False) to circusd to analyze problems (default: False) - **proc_name** -- the arbiter process name - **fqdn_prefix** -- a prefix for the unique identifier of the circus instance on the cluster. - **endpoint_owner** -- unix user to chown the endpoint to if using ipc. - **papa_endpoint** -- the papa process kernel endpoint """ def __init__(self, watchers, endpoint, pubsub_endpoint, check_delay=1.0, prereload_fn=None, context=None, loop=None, statsd=False, stats_endpoint=None, statsd_close_outputs=False, multicast_endpoint=None, plugins=None, sockets=None, warmup_delay=0, httpd=False, httpd_host='localhost', httpd_port=8080, httpd_close_outputs=False, debug=False, debug_gc=False, ssh_server=None, proc_name='circusd', pidfile=None, loglevel=None, logoutput=None, loggerconfig=None, fqdn_prefix=None, umask=None, endpoint_owner=None, papa_endpoint=None): self.watchers = watchers self.endpoint = endpoint self.check_delay = check_delay self.prereload_fn = prereload_fn self.pubsub_endpoint = pubsub_endpoint self.multicast_endpoint = multicast_endpoint self.proc_name = proc_name self.ssh_server = ssh_server self.evpub_socket = None self.pidfile = pidfile self.loglevel = loglevel self.logoutput = logoutput self.loggerconfig = loggerconfig self.umask = umask self.endpoint_owner = endpoint_owner self._running = False try: # getfqdn appears to fail in Python3.3 in the unittest # framework so fall back to gethostname socket_fqdn = socket.getfqdn() except KeyError: socket_fqdn = socket.gethostname() if fqdn_prefix is None: fqdn = socket_fqdn else: fqdn = '{}@{}'.format(fqdn_prefix, socket_fqdn) self.fqdn = fqdn if papa_endpoint and papa: if papa_endpoint.startswith('ipc:/'): papa_endpoint = papa_endpoint[4:] while papa_endpoint[:2] == '//': papa_endpoint = papa_endpoint[1:] papa.set_default_path(papa_endpoint) elif papa_endpoint.startswith('tcp://'): papa_endpoint = papa_endpoint[6:].partition(':')[2] papa.set_default_port = papa_endpoint self.ctrl = self.loop = None self._provided_loop = False self.socket_event = False if loop is not None: self._provided_loop = True self.loop = loop # initialize zmq context self._init_context(context) self.pid = os.getpid() self._watchers_names = {} self._stopping = False self._restarting = False self.debug = debug self._exclusive_running_command = None if self.debug: self.stdout_stream = self.stderr_stream = {'class': 'StdoutStream'} else: self.stdout_stream = self.stderr_stream = None self.debug_gc = debug_gc if debug_gc: gc.set_debug(gc.DEBUG_LEAK) # initializing circusd-stats as a watcher when configured self.statsd = statsd self.stats_endpoint = stats_endpoint if self.statsd: cmd = "%s -c 'from circus import stats; stats.main()'" % \ sys.executable cmd += ' --endpoint %s' % self.endpoint cmd += ' --pubsub %s' % self.pubsub_endpoint cmd += ' --statspoint %s' % self.stats_endpoint if ssh_server is not None: cmd += ' --ssh %s' % ssh_server if debug: cmd += ' --log-level DEBUG' elif self.loglevel: cmd += ' --log-level ' + self.loglevel if self.logoutput: cmd += ' --log-output ' + self.logoutput stats_watcher = Watcher('circusd-stats', cmd, use_sockets=True, singleton=True, stdout_stream=self.stdout_stream, stderr_stream=self.stderr_stream, copy_env=True, copy_path=True, close_child_stderr=statsd_close_outputs, close_child_stdout=statsd_close_outputs) self.watchers.append(stats_watcher) # adding the httpd if httpd: # adding the socket httpd_socket = CircusSocket(name='circushttpd', host=httpd_host, port=httpd_port) if sockets is None: sockets = [httpd_socket] else: sockets.append(httpd_socket) cmd = ("%s -c 'from circusweb import circushttpd; " "circushttpd.main()'") % sys.executable cmd += ' --endpoint %s' % self.endpoint cmd += ' --fd $(circus.sockets.circushttpd)' if ssh_server is not None: cmd += ' --ssh %s' % ssh_server # Adding the watcher httpd_watcher = Watcher('circushttpd', cmd, use_sockets=True, singleton=True, stdout_stream=self.stdout_stream, stderr_stream=self.stderr_stream, copy_env=True, copy_path=True, close_child_stderr=httpd_close_outputs, close_child_stdout=httpd_close_outputs) self.watchers.append(httpd_watcher) # adding each plugin as a watcher ch_stderr = self.stderr_stream is None ch_stdout = self.stdout_stream is None if plugins is not None: for plugin in plugins: fqn = plugin['use'] cmd = get_plugin_cmd(plugin, self.endpoint, self.pubsub_endpoint, self.check_delay, ssh_server, debug=self.debug, loglevel=self.loglevel, logoutput=self.logoutput) plugin_cfg = dict(cmd=cmd, priority=1, singleton=True, stdout_stream=self.stdout_stream, stderr_stream=self.stderr_stream, copy_env=True, copy_path=True, close_child_stderr=ch_stderr, close_child_stdout=ch_stdout) plugin_cfg.update(plugin) if 'name' not in plugin_cfg: plugin_cfg['name'] = fqn plugin_watcher = Watcher.load_from_config(plugin_cfg) self.watchers.append(plugin_watcher) self.sockets = CircusSockets(sockets) self.warmup_delay = warmup_delay @property def running(self): return self._running def _init_context(self, context): self.context = context or zmq.Context.instance() if self.loop is None: ioloop.install() self.loop = ioloop.IOLoop.instance() self.ctrl = Controller(self.endpoint, self.multicast_endpoint, self.context, self.loop, self, self.check_delay, self.endpoint_owner) def get_socket(self, name): return self.sockets.get(name, None) def get_socket_config(self, config, name): for i in config.get('sockets', []): if i['name'] == name: return i.copy() return None def get_watcher_config(self, config, name): for i in config.get('watchers', []): if i['name'] == name: return i.copy() return None def get_plugin_config(self, config, name): for i in config.get('plugins', []): if i['name'] == name: cfg = i.copy() cmd = get_plugin_cmd(cfg, self.endpoint, self.pubsub_endpoint, self.check_delay, self.ssh_server, debug=self.debug) cfg.update(dict(cmd=cmd, priority=1, singleton=True, stdout_stream=self.stdout_stream, stderr_stream=self.stderr_stream, copy_env=True, copy_path=True)) return cfg return None @classmethod def get_arbiter_config(cls, config): cfg = config.copy() del cfg['watchers'] del cfg['plugins'] del cfg['sockets'] return cfg @synchronized("arbiter_reload_config") @gen.coroutine def reload_from_config(self, config_file=None, inside_circusd=False): new_cfg = get_config(config_file if config_file else self.config_file) # if arbiter is changed, reload everything if self.get_arbiter_config(new_cfg) != self._cfg: yield self._restart(inside_circusd=inside_circusd) return ignore_sn = set(['circushttpd']) ignore_wn = set(['circushttpd', 'circusd-stats']) # Gather socket names. current_sn = set([i.name for i in self.sockets.values()]) - ignore_sn new_sn = set([i['name'] for i in new_cfg.get('sockets', [])]) added_sn = new_sn - current_sn deleted_sn = current_sn - new_sn maybechanged_sn = current_sn - deleted_sn changed_sn = set([]) wn_with_changed_socket = set([]) wn_with_deleted_socket = set([]) # get changed sockets for n in maybechanged_sn: s = self.get_socket(n) if self.get_socket_config(new_cfg, n) != s._cfg: changed_sn.add(n) # just delete the socket and add it again deleted_sn.add(n) added_sn.add(n) # Get the watchers whichs use these, so they could be # deleted and added also for w in self.iter_watchers(): if 'circus.sockets.%s' % n.lower() in w.cmd: wn_with_changed_socket.add(w.name) # get deleted sockets for n in deleted_sn: s = self.get_socket(n) s.close() # Get the watchers whichs use these, these should not be # active anymore for w in self.iter_watchers(): if 'circus.sockets.%s' % n.lower() in w.cmd: wn_with_deleted_socket.add(w.name) del self.sockets[s.name] # get added sockets for n in added_sn: socket_config = self.get_socket_config(new_cfg, n) s = CircusSocket.load_from_config(socket_config) s.bind_and_listen() self.sockets[s.name] = s if added_sn or deleted_sn: # make sure all existing watchers get the new sockets in # their attributes and get the old removed # XXX: is this necessary? self.sockets is an mutable # object for watcher in self.iter_watchers(): # XXX: What happens as initalize is called on a # running watcher? watcher.initialize(self.evpub_socket, self.sockets, self) # Gather watcher names. current_wn = set([i.name for i in self.iter_watchers()]) - ignore_wn new_wn = set([i['name'] for i in new_cfg.get('watchers', [])]) new_wn = new_wn | set([i['name'] for i in new_cfg.get('plugins', [])]) added_wn = (new_wn - current_wn) | wn_with_changed_socket deleted_wn = current_wn - new_wn - wn_with_changed_socket maybechanged_wn = current_wn - deleted_wn changed_wn = set([]) if wn_with_deleted_socket and wn_with_deleted_socket not in new_wn: raise ValueError('Watchers %s uses a socket which is deleted' % wn_with_deleted_socket) # get changed watchers for n in maybechanged_wn: w = self.get_watcher(n) new_watcher_cfg = (self.get_watcher_config(new_cfg, n) or self.get_plugin_config(new_cfg, n)) old_watcher_cfg = w._cfg.copy() if 'env' in new_watcher_cfg: new_watcher_cfg['env'] = parse_env_dict(new_watcher_cfg['env']) # discarding env exceptions for key in _ENV_EXCEPTIONS: if 'env' in new_watcher_cfg and key in new_watcher_cfg['env']: del new_watcher_cfg['env'][key] if 'env' in new_watcher_cfg and key in old_watcher_cfg['env']: del old_watcher_cfg['env'][key] diff = DictDiffer(new_watcher_cfg, old_watcher_cfg).changed() if diff == set(['numprocesses']): # if nothing but the number of processes is # changed, just changes this w.set_numprocesses(int(new_watcher_cfg['numprocesses'])) changed = False else: changed = len(diff) > 0 if changed: # Others things are changed. Just delete and add the watcher. changed_wn.add(n) deleted_wn.add(n) added_wn.add(n) # delete watchers for n in deleted_wn: w = self.get_watcher(n) yield w._stop() del self._watchers_names[w.name.lower()] self.watchers.remove(w) # add watchers for n in added_wn: new_watcher_cfg = (self.get_plugin_config(new_cfg, n) or self.get_watcher_config(new_cfg, n)) w = Watcher.load_from_config(new_watcher_cfg) w.initialize(self.evpub_socket, self.sockets, self) yield self.start_watcher(w) self.watchers.append(w) self._watchers_names[w.name.lower()] = w @classmethod def load_from_config(cls, config_file, loop=None): cfg = get_config(config_file) watchers = [] for watcher in cfg.get('watchers', []): watchers.append(Watcher.load_from_config(watcher)) sockets = [] for socket_ in cfg.get('sockets', []): sockets.append(CircusSocket.load_from_config(socket_)) httpd = cfg.get('httpd', False) if httpd: # controlling that we have what it takes to run the web UI # if something is missing this will tell the user try: import circusweb # NOQA except ImportError: logger.error('You need to install circus-web') sys.exit(1) # creating arbiter arbiter = cls(watchers, cfg['endpoint'], cfg['pubsub_endpoint'], check_delay=cfg.get('check_delay', 1.), prereload_fn=cfg.get('prereload_fn'), statsd=cfg.get('statsd', False), stats_endpoint=cfg.get('stats_endpoint'), multicast_endpoint=cfg.get('multicast_endpoint'), plugins=cfg.get('plugins'), sockets=sockets, warmup_delay=cfg.get('warmup_delay', 0), httpd=httpd, loop=loop, httpd_host=cfg.get('httpd_host', 'localhost'), httpd_port=cfg.get('httpd_port', 8080), debug=cfg.get('debug', False), debug_gc=cfg.get('debug_gc', False), ssh_server=cfg.get('ssh_server', None), pidfile=cfg.get('pidfile', None), loglevel=cfg.get('loglevel', None), logoutput=cfg.get('logoutput', None), loggerconfig=cfg.get('loggerconfig', None), fqdn_prefix=cfg.get('fqdn_prefix', None), umask=cfg['umask'], endpoint_owner=cfg.get('endpoint_owner', None)) # store the cfg which will be used, so it can be used later # for checking if the cfg has been changed arbiter._cfg = cls.get_arbiter_config(cfg) arbiter.config_file = config_file return arbiter def iter_watchers(self, reverse=True): return sorted(self.watchers, key=lambda a: a.priority, reverse=reverse) @debuglog def initialize(self): # set process title _setproctitle(self.proc_name) # set umask even though we may have already set it early in circusd.py if self.umask is not None: os.umask(self.umask) # event pub socket self.evpub_socket = self.context.socket(zmq.PUB) self.evpub_socket.bind(self.pubsub_endpoint) self.evpub_socket.linger = 0 # initialize sockets if len(self.sockets) > 0: self.sockets.bind_and_listen_all() logger.info("sockets started") # initialize watchers for watcher in self.iter_watchers(): self._watchers_names[watcher.name.lower()] = watcher watcher.initialize(self.evpub_socket, self.sockets, self) @gen.coroutine def start_watcher(self, watcher): """Aska a specific watcher to start and wait for the specified warmup delay.""" if watcher.autostart: yield watcher._start() yield tornado_sleep(self.warmup_delay) @gen.coroutine @debuglog def start(self, cb=None): """Starts all the watchers. If the ioloop has been provided during __init__() call, starts all watchers as a standard coroutine If the ioloop hasn't been provided during __init__() call (default), starts all watchers and the eventloop (and blocks here). In this mode the method MUST NOT yield anything because it's called as a standard method. :param cb: Callback called after all the watchers have been started, when the loop hasn't been provided. :type function: """ logger.info("Starting master on pid %s", self.pid) self.initialize() # start controller self.ctrl.start() self._restarting = False try: # initialize processes logger.debug('Initializing watchers') if self._provided_loop: yield self.start_watchers() else: # start_watchers will be called just after the start_io_loop() if not cb: def cb(x): pass self.loop.add_future(self.start_watchers(), cb) logger.info('Arbiter now waiting for commands') self._running = True if not self._provided_loop: # If an event loop is not provided, block at this line self.start_io_loop() finally: if not self._provided_loop: # If an event loop is not provided, do some cleaning self.stop_controller_and_close_sockets() raise gen.Return(self._restarting) def stop_controller_and_close_sockets(self): self.ctrl.stop() self.evpub_socket.close() if len(self.sockets) > 0: self.sockets.close_all() self._running = False def start_io_loop(self): """Starts the ioloop and wait inside it """ while True: try: self.loop.start() except zmq.ZMQError as e: if e.errno == errno.EINTR: continue else: raise else: break @synchronized("arbiter_stop") @gen.coroutine def stop(self): yield self._stop(True) @gen.coroutine def _emergency_stop(self): """Emergency and fast stop, to use only in circusd """ for watcher in self.iter_watchers(): watcher.graceful_timeout = 0 yield self._stop_watchers() self.stop_controller_and_close_sockets() @gen.coroutine def _stop(self, for_shutdown=False): logger.info('Arbiter exiting') self._stopping = True yield self._stop_watchers(close_output_streams=True, for_shutdown=for_shutdown) if self._provided_loop: cb = self.stop_controller_and_close_sockets self.loop.add_callback(cb) else: # stop_controller_and_close_sockets will be # called in the end of start() method self.loop.add_callback(self.loop.stop) def reap_processes(self): # map watcher to pids watchers_pids = {} for watcher in self.iter_watchers(): if not watcher.is_stopped(): for process in watcher.processes.values(): watchers_pids[process.pid] = watcher # detect dead children if not IS_WINDOWS: while True: try: # wait for our child (so it's not a zombie) pid, status = os.waitpid(-1, os.WNOHANG) if not pid: break if pid in watchers_pids: watcher = watchers_pids[pid] watcher.reap_process(pid, status) except OSError as e: if e.errno == errno.EAGAIN: sleep(0) continue elif e.errno == errno.ECHILD: # process already reaped return else: raise @synchronized("manage_watchers") @gen.coroutine def manage_watchers(self): if self._stopping: return need_on_demand = False # manage and reap processes self.reap_processes() list_to_yield = [] for watcher in self.iter_watchers(): if watcher.on_demand and watcher.is_stopped(): need_on_demand = True list_to_yield.append(watcher.manage_processes()) if len(list_to_yield) > 0: yield list_to_yield if need_on_demand: sockets = [x.fileno() for x in self.sockets.values()] rlist, wlist, xlist = select.select(sockets, [], [], 0) if rlist: self.socket_event = True self._start_watchers() self.socket_event = False @synchronized("arbiter_reload") @gen.coroutine @debuglog def reload(self, graceful=True, sequential=False): """Reloads everything. Run the :func:`prereload_fn` callable if any, then gracefuly reload all watchers. """ if self._stopping: return if self.prereload_fn is not None: self.prereload_fn(self) # reopen log files for handler in logger.handlers: if isinstance(handler, logging.FileHandler): handler.acquire() handler.stream.close() handler.stream = open(handler.baseFilename, handler.mode) handler.release() # gracefully reload watchers for watcher in self.iter_watchers(): yield watcher._reload(graceful=graceful, sequential=sequential) tornado_sleep(self.warmup_delay) def numprocesses(self): """Return the number of processes running across all watchers.""" return sum([len(watcher) for watcher in self.watchers]) def numwatchers(self): """Return the number of watchers.""" return len(self.watchers) def get_watcher(self, name): """Return the watcher *name*.""" return self._watchers_names[name.lower()] def statuses(self): return dict([(watcher.name, watcher.status()) for watcher in self.watchers]) @synchronized("arbiter_add_watcher") def add_watcher(self, name, cmd, **kw): """Adds a watcher. Options: - **name**: name of the watcher to add - **cmd**: command to run. - all other options defined in the Watcher constructor. """ if name in self._watchers_names: raise AlreadyExist("%r already exist" % name) if not name: return ValueError("command name shouldn't be empty") watcher = Watcher(name, cmd, **kw) if self.evpub_socket is not None: watcher.initialize(self.evpub_socket, self.sockets, self) self.watchers.append(watcher) self._watchers_names[watcher.name.lower()] = watcher return watcher @synchronized("arbiter_rm_watcher") @gen.coroutine def rm_watcher(self, name, nostop=False): """Deletes a watcher. Options: - **name**: name of the watcher to delete """ logger.debug('Deleting %r watcher', name) # remove the watcher from the list watcher = self._watchers_names.pop(name.lower()) del self.watchers[self.watchers.index(watcher)] if not nostop: # stop the watcher yield watcher._stop() @synchronized("arbiter_start_watchers") @gen.coroutine def start_watchers(self, watcher_iter_func=None): yield self._start_watchers(watcher_iter_func=watcher_iter_func) @gen.coroutine def _start_watchers(self, watcher_iter_func=None): if watcher_iter_func is None: watchers = self.iter_watchers() else: watchers = watcher_iter_func() for watcher in watchers: if watcher.autostart: yield watcher._start() yield tornado_sleep(self.warmup_delay) @gen.coroutine @debuglog def _stop_watchers(self, close_output_streams=False, watcher_iter_func=None, for_shutdown=False): if watcher_iter_func is None: watchers = self.iter_watchers(reverse=False) else: watchers = watcher_iter_func(reverse=False) yield [w._stop(close_output_streams, for_shutdown) for w in watchers] @synchronized("arbiter_stop_watchers") @gen.coroutine def stop_watchers(self, watcher_iter_func=None): yield self._stop_watchers(watcher_iter_func=watcher_iter_func) @gen.coroutine def _restart(self, inside_circusd=False, watcher_iter_func=None): if inside_circusd: self._restarting = True yield self._stop() else: yield self._stop_watchers(watcher_iter_func=watcher_iter_func) yield self._start_watchers(watcher_iter_func=watcher_iter_func) @synchronized("arbiter_restart") @gen.coroutine def restart(self, inside_circusd=False, watcher_iter_func=None): yield self._restart(inside_circusd=inside_circusd, watcher_iter_func=watcher_iter_func) @property def endpoint_owner_mode(self): return self.ctrl.endpoint_owner_mode # just wrap the controller class ThreadedArbiter(Thread, Arbiter): def __init__(self, *args, **kw): Thread.__init__(self) Arbiter.__init__(self, *args, **kw) def start(self): return Thread.start(self) def run(self): return Arbiter.start(self) def stop(self): Arbiter.stop(self) if get_ident() != self.ident and self.isAlive(): self.join() circus-0.12.1/circus/circusctl.py000066400000000000000000000316201256046442300167300ustar00rootroot00000000000000# -*- coding: utf-8 - import argparse import cmd import getopt import json import logging import os import sys import textwrap import traceback import shlex # import pygments if here try: import pygments # NOQA from pygments.lexers import get_lexer_for_mimetype from pygments.formatters import TerminalFormatter except ImportError: pygments = False # NOQA from circus import __version__ from circus.client import CircusClient from circus.commands import get_commands from circus.consumer import CircusConsumer from circus.exc import CallError, ArgumentError from circus.util import DEFAULT_ENDPOINT_SUB, DEFAULT_ENDPOINT_DEALER USAGE = 'circusctl [options] command [args]' VERSION = 'circusctl ' + __version__ TIMEOUT_MSG = """\ A time out usually happens in one of those cases: #1 The Circus daemon could not be reached. #2 The Circus daemon took too long to perform the operation For #1, make sure you are hitting the right place by checking your --endpoint option. For #2, if you are not expecting a result to come back, increase your timeout option value (particularly with waiting switches) """ def prettify(jsonobj, prettify=True): """ prettiffy JSON output """ if not prettify: return json.dumps(jsonobj) json_str = json.dumps(jsonobj, indent=2, sort_keys=True) if pygments: try: lexer = get_lexer_for_mimetype("application/json") return pygments.highlight(json_str, lexer, TerminalFormatter()) except: pass return json_str class _Help(argparse.HelpFormatter): commands = None def _metavar_formatter(self, action, default_metavar): if action.dest != 'command': return super(_Help, self)._metavar_formatter(action, default_metavar) commands = sorted(self.commands.items()) max_len = max([len(name) for name, help in commands]) output = [] for name, command in commands: output.append('\t%-*s\t%s' % (max_len, name, command.short)) def format(tuple_size): res = '\n'.join(output) return (res, ) * tuple_size return format def start_section(self, heading): if heading == 'positional arguments': heading = 'Commands' super(_Help, self).start_section(heading) def _get_switch_str(opt): """ Output just the '-r, --rev [VAL]' part of the option string. """ if opt[2] is None or opt[2] is True or opt[2] is False: default = "" else: default = "[VAL]" if opt[0]: # has a short and long option return "-%s, --%s %s" % (opt[0], opt[1], default) else: # only has a long option return "--%s %s" % (opt[1], default) class ControllerApp(object): def __init__(self, commands, client=None): self.commands = commands self.client = client def run(self, args): try: return self.dispatch(args) except getopt.GetoptError as e: print("Error: %s\n" % str(e)) self.display_help() return 2 except CallError as e: sys.stderr.write("%s\n" % str(e)) return 1 except ArgumentError as e: sys.stderr.write("%s\n" % str(e)) return 1 except KeyboardInterrupt: return 1 except Exception: sys.stderr.write(traceback.format_exc()) return 1 def dispatch(self, args): opts = {} command = self.commands[args.command] for option in command.options: name = option[1] if name in args: opts[name] = getattr(args, name) if args.help: print(textwrap.dedent(command.__doc__)) return 0 else: if hasattr(args, 'start'): opts['start'] = args.start if args.endpoint is None and command.msg_type != 'dealer': if command.msg_type == 'sub': args.endpoint = DEFAULT_ENDPOINT_SUB else: args.endpoint = DEFAULT_ENDPOINT_DEALER msg = command.message(*args.args, **opts) handler = getattr(self, "handle_%s" % command.msg_type) return handler(command, self.globalopts, msg, args.endpoint, int(args.timeout), args.ssh, args.ssh_keyfile) def handle_sub(self, command, opts, topics, endpoint, timeout, ssh_server, ssh_keyfile): consumer = CircusConsumer(topics, endpoint=endpoint) for topic, msg in consumer: print("%s: %s" % (topic, msg)) return 0 def _console(self, client, command, opts, msg): if opts['json']: return prettify(client.call(msg), prettify=opts['prettify']) else: return command.console_msg(client.call(msg)) def handle_dealer(self, command, opts, msg, endpoint, timeout, ssh_server, ssh_keyfile): if endpoint is not None: client = CircusClient(endpoint=endpoint, timeout=timeout, ssh_server=ssh_server, ssh_keyfile=ssh_keyfile) else: client = self.client try: if isinstance(msg, list): for i, c in enumerate(msg): clm = self._console(client, c['cmd'], opts, c['msg']) print("%s: %s" % (i, clm)) else: print(self._console(client, command, opts, msg)) except CallError as e: msg = str(e) if 'timed out' in str(e).lower(): msg += TIMEOUT_MSG sys.stderr.write(msg) return 1 finally: if endpoint is not None: client.stop() return 0 class CircusCtl(cmd.Cmd, object): """CircusCtl tool.""" prompt = '(circusctl) ' def __new__(cls, client, commands, *args, **kw): """Auto add do and complete methods for all known commands.""" cls.commands = commands cls.controller = ControllerApp(commands, client) cls.client = client for name, command in commands.items(): cls._add_do_cmd(name, command) cls._add_complete_cmd(name, command) return super(CircusCtl, cls).__new__(cls, *args, **kw) def __init__(self, client, *args, **kwargs): super(CircusCtl, self).__init__() @classmethod def _add_do_cmd(cls, cmd_name, command): def inner_do_cmd(cls, line): arguments = parse_arguments([cmd_name] + shlex.split(line), cls.commands) cls.controller.run(arguments['args']) inner_do_cmd.__doc__ = textwrap.dedent(command.__doc__) inner_do_cmd.__name__ = "do_%s" % cmd_name setattr(cls, inner_do_cmd.__name__, inner_do_cmd) @classmethod def _add_complete_cmd(cls, cmd_name, command): def inner_complete_cmd(cls, *args, **kwargs): if hasattr(command, 'autocomplete'): try: return command.autocomplete(cls.client, *args, **kwargs) except Exception as e: sys.stderr.write(str(e) + "\n") traceback.print_exc(file=sys.stderr) else: return [] inner_complete_cmd.__doc__ = "Complete the %s command" % cmd_name inner_complete_cmd.__name__ = "complete_%s" % cmd_name setattr(cls, inner_complete_cmd.__name__, inner_complete_cmd) def do_EOF(self, line): return True def postloop(self): sys.stdout.write('\n') def autocomplete(self, autocomplete=False, words=None, cword=None): """ Output completion suggestions for BASH. The output of this function is passed to BASH's `COMREPLY` variable and treated as completion suggestions. `COMREPLY` expects a space separated string as the result. The `COMP_WORDS` and `COMP_CWORD` BASH environment variables are used to get information about the cli input. Please refer to the BASH man-page for more information about this variables. Subcommand options are saved as pairs. A pair consists of the long option string (e.g. '--exclude') and a boolean value indicating if the option requires arguments. When printing to stdout, a equal sign is appended to options which require arguments. Note: If debugging this function, it is recommended to write the debug output in a separate file. Otherwise the debug output will be treated and formatted as potential completion suggestions. """ autocomplete = autocomplete or 'AUTO_COMPLETE' in os.environ # Don't complete if user hasn't sourced bash_completion file. if not autocomplete: return words = words or os.environ['COMP_WORDS'].split()[1:] cword = cword or int(os.environ['COMP_CWORD']) try: curr = words[cword - 1] except IndexError: curr = '' subcommands = get_commands() if cword == 1: # if completing the command name print(' '.join(sorted([x for x in subcommands if x.startswith(curr)]))) sys.exit(1) def start(self, globalopts): self.autocomplete() self.controller.globalopts = globalopts args = globalopts['args'] parser = globalopts['parser'] if hasattr(args, 'command'): sys.exit(self.controller.run(globalopts['args'])) if args.help: for command in sorted(self.commands.keys()): doc = textwrap.dedent(self.commands[command].__doc__) help = doc.split('\n')[0] parser.add_argument(command, help=help) parser.print_help() sys.exit(0) # no command, no --help: enter the CLI print(VERSION) self.do_status('') try: self.cmdloop() except KeyboardInterrupt: sys.stdout.write('\n') sys.exit(0) def parse_arguments(args, commands): _Help.commands = commands options = { 'endpoint': {'default': None, 'help': 'connection endpoint'}, 'timeout': {'default': 5, 'help': 'connection timeout', 'type': int}, 'help': { 'default': False, 'action': 'store_true', 'help': 'Show help and exit'}, 'json': {'default': False, 'action': 'store_true', 'help': 'output to JSON'}, 'prettify': { 'default': False, 'action': 'store_true', 'help': 'prettify output'}, 'ssh': { 'default': None, 'help': 'SSH Server in the format user@host:port'}, 'ssh_keyfile': { 'default': None, 'help': 'the path to the keyfile to authorise the user'}, 'version': { 'default': False, 'action': 'version', 'version': VERSION, 'help': 'display version and exit'}, } parser = argparse.ArgumentParser( description="Controls a Circus daemon", formatter_class=_Help, usage=USAGE, add_help=False) for option in sorted(options.keys()): parser.add_argument('--' + option, **options[option]) if any([value in commands for value in args]): subparsers = parser.add_subparsers(dest='command') for command, klass in commands.items(): subparser = subparsers.add_parser(command) subparser.add_argument('args', nargs="*", help=argparse.SUPPRESS) for option in klass.options: __, name, default, desc = option if isinstance(default, bool): action = 'store_true' else: action = 'store' subparser.add_argument('--' + name, action=action, default=default, help=desc) args = parser.parse_args(args) globalopts = {'args': args, 'parser': parser} for option in options: globalopts[option] = getattr(args, option) return globalopts def main(): logging.basicConfig() # TODO, we should ask the server for its command list commands = get_commands() globalopts = parse_arguments(sys.argv[1:], commands) if globalopts['endpoint'] is None: globalopts['endpoint'] = os.environ.get('CIRCUSCTL_ENDPOINT', DEFAULT_ENDPOINT_DEALER) client = CircusClient(endpoint=globalopts['endpoint'], timeout=globalopts['timeout'], ssh_server=globalopts['ssh'], ssh_keyfile=globalopts['ssh_keyfile']) CircusCtl(client, commands).start(globalopts) if __name__ == '__main__': main() circus-0.12.1/circus/circusd.py000066400000000000000000000122571256046442300163760ustar00rootroot00000000000000import sys import argparse import os try: import resource except ImportError: resource = None # NOQA from circus import logger from circus.arbiter import Arbiter from circus.pidfile import Pidfile from circus import __version__ from circus.util import MAXFD, REDIRECT_TO, configure_logger, LOG_LEVELS from circus.util import check_future_exception_and_log def get_maxfd(): if not resource: maxfd = MAXFD else: maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] if maxfd == resource.RLIM_INFINITY: maxfd = MAXFD return maxfd try: from os import closerange except ImportError: def closerange(fd_low, fd_high): # NOQA # Iterate through and close all file descriptors. for fd in range(fd_low, fd_high): try: os.close(fd) except OSError: # ERROR, fd wasn't open to begin with (ignored) pass # http://www.svbug.com/documentation/comp.unix.programmer-FAQ/faq_2.html#SEC16 def daemonize(): """Standard daemonization of a process. """ # guard to prevent daemonization with gevent loaded for module in sys.modules.keys(): if module.startswith('gevent'): raise ValueError('Cannot daemonize if gevent is loaded') if hasattr(os, 'fork'): child_pid = os.fork() else: raise ValueError("Daemonizing is not available on this platform.") if child_pid != 0: # we're in the parent os._exit(0) # child process os.setsid() subchild = os.fork() if subchild: os._exit(0) # subchild maxfd = get_maxfd() closerange(0, maxfd) os.open(REDIRECT_TO, os.O_RDWR) os.dup2(0, 1) os.dup2(0, 2) def main(): import zmq try: zmq_version = [int(part) for part in zmq.__version__.split('.')[:2]] if len(zmq_version) < 2: raise ValueError() except (AttributeError, ValueError): print('Unknown PyZQM version - aborting...') sys.exit(0) if zmq_version[0] < 13 or (zmq_version[0] == 13 and zmq_version[1] < 1): print('circusd needs PyZMQ >= 13.1.0 to run - aborting...') sys.exit(0) parser = argparse.ArgumentParser(description='Run some watchers.') parser.add_argument('config', help='configuration file', nargs='?') # XXX we should be able to add all these options in the config file as well parser.add_argument('--log-level', dest='loglevel', choices=list(LOG_LEVELS.keys()) + [ key.upper() for key in LOG_LEVELS.keys()], help="log level") parser.add_argument('--log-output', dest='logoutput', help=( "The location where the logs will be written. The default behavior " "is to write to stdout (you can force it by passing '-' to " "this option). Takes a filename otherwise.")) parser.add_argument("--logger-config", dest="loggerconfig", help=( "The location where a standard Python logger configuration INI, " "JSON or YAML file can be found. This can be used to override " "the default logging configuration for the arbiter.")) parser.add_argument('--daemon', dest='daemonize', action='store_true', help="Start circusd in the background. Not supported " "on Windows") parser.add_argument('--pidfile', dest='pidfile') parser.add_argument('--version', action='store_true', default=False, help='Displays Circus version and exits.') args = parser.parse_args() if args.version: print(__version__) sys.exit(0) if args.config is None: parser.print_usage() sys.exit(0) if args.daemonize: daemonize() # From here it can also come from the arbiter configuration # load the arbiter from config arbiter = Arbiter.load_from_config(args.config) # go ahead and set umask early if it is in the config if arbiter.umask is not None: os.umask(arbiter.umask) pidfile = args.pidfile or arbiter.pidfile or None if pidfile: pidfile = Pidfile(pidfile) try: pidfile.create(os.getpid()) except RuntimeError as e: print(str(e)) sys.exit(1) # configure the logger loglevel = args.loglevel or arbiter.loglevel or 'info' logoutput = args.logoutput or arbiter.logoutput or '-' loggerconfig = args.loggerconfig or arbiter.loggerconfig or None configure_logger(logger, loglevel, logoutput, loggerconfig) # Main loop restart = True while restart: try: arbiter = arbiter or Arbiter.load_from_config(args.config) future = arbiter.start() restart = False if check_future_exception_and_log(future) is None: restart = arbiter._restarting except Exception as e: # emergency stop arbiter.loop.run_sync(arbiter._emergency_stop) raise(e) except KeyboardInterrupt: pass finally: arbiter = None if pidfile is not None: pidfile.unlink() sys.exit(0) if __name__ == '__main__': main() circus-0.12.1/circus/client.py000066400000000000000000000114661256046442300162210ustar00rootroot00000000000000 # -*- coding: utf-8 - import errno import uuid import zmq import zmq.utils.jsonapi as json from zmq.eventloop.zmqstream import ZMQStream import tornado from circus.exc import CallError from circus.py3compat import string_types, b from circus.util import DEFAULT_ENDPOINT_DEALER, get_connection def make_message(command, **props): return {"command": command, "properties": props or {}} def cast_message(command, **props): return {"command": command, "msg_type": "cast", "properties": props or {}} def make_json(command, **props): return json.dumps(make_message(command, **props)) class AsyncCircusClient(object): def __init__(self, context=None, endpoint=DEFAULT_ENDPOINT_DEALER, timeout=5.0, ssh_server=None, ssh_keyfile=None): self._init_context(context) self.endpoint = endpoint self._id = b(uuid.uuid4().hex) self.socket = self.context.socket(zmq.DEALER) self.socket.setsockopt(zmq.IDENTITY, self._id) self.socket.setsockopt(zmq.LINGER, 0) get_connection(self.socket, endpoint, ssh_server, ssh_keyfile) self._timeout = timeout self.timeout = timeout * 1000 self.stream = ZMQStream(self.socket, tornado.ioloop.IOLoop.instance()) def _init_context(self, context): self.context = context or zmq.Context.instance() def stop(self): self.stream.stop_on_recv() # only supported by libzmq >= 3 if hasattr(self.socket, 'disconnect'): self.socket.disconnect(self.endpoint) self.stream.close() def send_message(self, command, **props): return self.call(make_message(command, **props)) @tornado.gen.coroutine def call(self, cmd): if isinstance(cmd, string_types): raise DeprecationWarning('call() takes a mapping') call_id = uuid.uuid4().hex cmd['id'] = call_id try: cmd = json.dumps(cmd) except ValueError as e: raise CallError(str(e)) try: yield tornado.gen.Task(self.stream.send, cmd) except zmq.ZMQError as e: raise CallError(str(e)) while True: messages = yield tornado.gen.Task(self.stream.on_recv) for message in messages: try: res = json.loads(message) if res.get('id') != call_id: # we got the wrong message continue raise tornado.gen.Return(res) except ValueError as e: raise CallError(str(e)) class CircusClient(object): def __init__(self, context=None, endpoint=DEFAULT_ENDPOINT_DEALER, timeout=5.0, ssh_server=None, ssh_keyfile=None): self._init_context(context) self.endpoint = endpoint self._id = b(uuid.uuid4().hex) self.socket = self.context.socket(zmq.DEALER) self.socket.setsockopt(zmq.IDENTITY, self._id) self.socket.setsockopt(zmq.LINGER, 0) get_connection(self.socket, endpoint, ssh_server, ssh_keyfile) self._init_poller() self._timeout = timeout self.timeout = timeout * 1000 def _init_context(self, context): self.context = context or zmq.Context.instance() def _init_poller(self): self.poller = zmq.Poller() self.poller.register(self.socket, zmq.POLLIN) def stop(self): # only supported by libzmq >= 3 if hasattr(self.socket, 'disconnect'): self.socket.disconnect(self.endpoint) self.socket.close() def send_message(self, command, **props): return self.call(make_message(command, **props)) def call(self, cmd): if isinstance(cmd, string_types): raise DeprecationWarning('call() takes a mapping') call_id = uuid.uuid4().hex cmd['id'] = call_id try: cmd = json.dumps(cmd) except ValueError as e: raise CallError(str(e)) try: self.socket.send(cmd) except zmq.ZMQError as e: raise CallError(str(e)) while True: try: events = dict(self.poller.poll(self.timeout)) except zmq.ZMQError as e: if e.errno == errno.EINTR: continue else: print(str(e)) raise CallError(str(e)) if len(events) == 0: raise CallError("Timed out.") for socket in events: msg = socket.recv() try: res = json.loads(msg) if res.get('id') != call_id: # we got the wrong message continue return res except ValueError as e: raise CallError(str(e)) circus-0.12.1/circus/commands/000077500000000000000000000000001256046442300161625ustar00rootroot00000000000000circus-0.12.1/circus/commands/__init__.py000066400000000000000000000006611256046442300202760ustar00rootroot00000000000000from circus.commands import ( # NOQA addwatcher, decrproc, dstats, get, globaloptions, incrproc, ipythonshell, list, listen, listsockets, numprocesses, numwatchers, options, quit, reload, reloadconfig, restart, rmwatcher, sendsignal, set, start, stats, status, stop ) from circus.commands.base import get_commands, ok, error # NOQA circus-0.12.1/circus/commands/addwatcher.py000066400000000000000000000067551256046442300206570ustar00rootroot00000000000000from circus.commands.base import Command from circus.commands.util import validate_option from circus.exc import ArgumentError, MessageError from circus.config import rlimit_value class AddWatcher(Command): """\ Add a watcher ============= This command add a watcher dynamically to a arbiter. ZMQ Message ----------- :: { "command": "add", "properties": { "cmd": "/path/to/commandline --option" "name": "nameofwatcher" "args": [], "options": {}, "start": false } } A message contains 2 properties: - cmd: Full command line to execute in a process - args: array, arguments passed to the command (optional) - name: name of watcher - options: options of a watcher - start: start the watcher after the creation The response return a status "ok". Command line ------------ :: $ circusctl add [--start] Options +++++++ - : name of the watcher to create - : full command line to execute in a process - --start: start the watcher immediately """ name = "add" options = [('', 'start', False, "start immediately the watcher")] properties = ['name', 'cmd'] def message(self, *args, **opts): if len(args) < 2: raise ArgumentError("Invalid number of arguments") return self.make_message(name=args[0], cmd=" ".join(args[1:]), start=opts.get('start', False)) def execute(self, arbiter, props): options = props.get('options', {}) # check for endpoint_owner uid restriction mode # it would be better to use some type of SO_PEERCRED lookup on the ipc # socket to get the uid of the client process and restrict on that, # but there's no good portable pythonic way of doing that right now # inside pyzmq or here. So we'll assume that the administrator has # set good rights on the ipc socket to help prevent privilege # escalation if arbiter.endpoint_owner_mode: cmd_uid = options.get('uid', None) if cmd_uid != arbiter.endpoint_owner: raise MessageError("uid does not match endpoint_owner") # convert all rlimit_* options into one rlimits dict which is required # by the watcher constructor (follows same pattern as config.py) rlimits = {} for key, val in options.items(): if key.startswith('rlimit_'): rlimits[key[7:]] = rlimit_value(val) if len(rlimits) > 0: options['rlimits'] = rlimits for key in rlimits.keys(): del options['rlimit_' + key] # now create and start the watcher watcher = arbiter.add_watcher(props['name'], props['cmd'], args=props.get('args'), **options) if props.get('start', False): return watcher.start() def validate(self, props): super(AddWatcher, self).validate(props) if 'options' in props: options = props.get('options') if not isinstance(options, dict): raise MessageError("'options' property should be an object") for key, val in props['options'].items(): validate_option(key, val) circus-0.12.1/circus/commands/base.py000066400000000000000000000056621256046442300174570ustar00rootroot00000000000000import copy import textwrap import time from circus.exc import MessageError from circus.commands import errors KNOWN_COMMANDS = [] def get_commands(): commands = {} for c in KNOWN_COMMANDS: cmd = c() commands[c.name] = cmd.copy() return commands def ok(props=None): resp = {"status": "ok", "time": time.time()} if props: resp.update(props) return resp def error(reason="unknown", tb=None, errno=errors.NOT_SPECIFIED): return { "status": "error", "reason": reason, "tb": tb, "time": time.time(), "errno": errno } class CommandMeta(type): def __new__(cls, name, bases, attrs): super_new = type.__new__ parents = [b for b in bases if isinstance(b, CommandMeta)] if not parents: return super_new(cls, name, bases, attrs) attrs["order"] = len(KNOWN_COMMANDS) new_class = super_new(cls, name, bases, attrs) new_class.fmt_desc() KNOWN_COMMANDS.append(new_class) return new_class def fmt_desc(cls): desc = textwrap.dedent(cls.__doc__).strip() setattr(cls, "desc", desc) setattr(cls, "short", desc.splitlines()[0]) class Command(object): name = None msg_type = "dealer" options = [] properties = [] waiting = False waiting_options = [('waiting', 'waiting', False, "Waiting the real end of the process")] ################################################## # These methods run within the circusctl process # ################################################## def make_message(self, **props): name = props.pop("command", self.name) return {"command": name, "properties": props or {}} def message(self, *args, **opts): raise NotImplementedError("message function isn't implemented") def console_error(self, msg): return "error: %s" % msg.get("reason") def console_msg(self, msg): if msg.get('status') == "ok": return "ok" return self.console_error(msg) def copy(self): return copy.copy(self) ################################################ # These methods run within the circusd process # ################################################ def execute(self, arbiter, props): raise NotImplementedError("execute function is not implemented") def _get_watcher(self, arbiter, watcher_name): """Get watcher from the arbiter if any.""" try: return arbiter.get_watcher(watcher_name.lower()) except KeyError: raise MessageError("program %s not found" % watcher_name) def validate(self, props): if not self.properties: return for propname in self.properties: if propname not in props: raise MessageError("message invalid %r is missing" % propname) Command = CommandMeta('Command', (Command,), {}) circus-0.12.1/circus/commands/decrproc.py000066400000000000000000000025751256046442300203460ustar00rootroot00000000000000from circus.commands.incrproc import IncrProc from circus.util import TransformableFuture class DecrProcess(IncrProc): """\ Decrement the number of processes in a watcher ============================================== This comment decrement the number of processes in a watcher by , 1 being the default. ZMQ Message ----------- :: { "command": "decr", "propeties": { "name": "" "nb": "waiting": False } } The response return the number of processes in the 'numprocesses` property:: { "status": "ok", "numprocesses": , "time", "timestamp" } Command line ------------ :: $ circusctl decr [] [--waiting] Options +++++++ - : name of the watcher - : the number of processes to remove. """ name = "decr" properties = ['name'] def execute(self, arbiter, props): watcher = self._get_watcher(arbiter, props.get('name')) nb = props.get('nb', 1) resp = TransformableFuture() resp.set_upstream_future(watcher.decr(nb)) resp.set_transform_function(lambda x: {"numprocesses": x}) return resp circus-0.12.1/circus/commands/dstats.py000066400000000000000000000037041256046442300200420ustar00rootroot00000000000000from circus.exc import ArgumentError from circus.commands.base import Command from circus.util import get_info _INFOLINE = ("%(pid)s %(cmdline)s %(username)s %(nice)s %(mem_info1)s " "%(mem_info2)s %(cpu)s %(mem)s %(ctime)s") class Daemontats(Command): """\ Get circusd stats ================= You can get at any time some statistics about circusd with the dstat command. ZMQ Message ----------- To get the circusd stats, simply run:: { "command": "dstats" } The response returns a mapping the property "infos" containing some process informations:: { "info": { "children": [], "cmdline": "python", "cpu": 0.1, "ctime": "0:00.41", "mem": 0.1, "mem_info1": "3M", "mem_info2": "2G", "nice": 0, "pid": 47864, "username": "root" }, "status": "ok", "time": 1332265655.897085 } Command Line ------------ :: $ circusctl dstats """ name = "dstats" def message(self, *args, **opts): if len(args) > 0: raise ArgumentError("Invalid message") return self.make_message() def execute(self, arbiter, props): return {'info': get_info(interval=0.01)} def _to_str(self, info): children = info.pop("children", []) ret = ['Main Process:', ' ' + _INFOLINE % info] if len(children) > 0: ret.append('Children:') for child in children: ret.append(' ' + _INFOLINE % child) return "\n".join(ret) def console_msg(self, msg): if msg['status'] == "ok": return self._to_str(msg['info']) else: return self.console_error(msg) circus-0.12.1/circus/commands/errors.py000066400000000000000000000002001256046442300200400ustar00rootroot00000000000000 NOT_SPECIFIED = 0 INVALID_JSON = 1 UNKNOWN_COMMAND = 2 MESSAGE_ERROR = 3 OS_ERROR = 4 COMMAND_ERROR = 5 BAD_MSG_DATA_ERROR = 6 circus-0.12.1/circus/commands/get.py000066400000000000000000000043201256046442300173120ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError, MessageError from circus.util import convert_opt class Get(Command): """\ Get the value of specific watcher options ========================================= This command can be used to query the current value of one or more watcher options. ZMQ Message ----------- :: { "command": "get", "properties": { "keys": ["key1, "key2"] "name": "nameofwatcher" } } A request message contains two properties: - keys: list, The option keys for which you want to get the values - name: name of watcher The response object has a property ``options`` which is a dictionary of option names and values. eg:: { "status": "ok", "options": { "graceful_timeout": 300, "send_hup": True, }, time': 1332202594.754644 } Command line ------------ :: $ circusctl get """ name = "get" properties = ['name', 'keys'] def message(self, *args, **opts): if len(args) < 2: raise ArgumentError("Invalid number of arguments") return self.make_message(name=args[0], keys=args[1:]) def execute(self, arbiter, props): watcher = self._get_watcher(arbiter, props.get('name')) # get options values. It return an error if one of the asked # options isn't found options = {} for name in props.get('keys', []): if name in watcher.optnames: options[name] = getattr(watcher, name) else: raise MessageError("%r option not found" % name) return {"options": options} def console_msg(self, msg): if msg['status'] == "ok": ret = [] for k, v in msg.get('options', {}).items(): ret.append("%s: %s" % (k, convert_opt(k, v))) return "\n".join(ret) return self.console_error(msg) circus-0.12.1/circus/commands/globaloptions.py000066400000000000000000000045551256046442300214210ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import MessageError from circus.util import convert_opt _OPTIONS = ('endpoint', 'stats_endpoint', 'pubsub_endpoint', 'check_delay', 'multicast_endpoint') class GlobalOptions(Command): """\ Get the arbiter options ======================= This command return the arbiter options ZMQ Message ----------- :: { "command": "globaloptions", "properties": { "key1": "val1", .. } } A message contains 2 properties: - keys: list, The option keys for which you want to get the values The response return an object with a property "options" containing the list of key/value returned by circus. eg:: { "status": "ok", "options": { "check_delay": 1, ... }, time': 1332202594.754644 } Command line ------------ :: $ circusctl globaloptions Options ------- Options Keys are: - endpoint: the controller ZMQ endpoint - pubsub_endpoint: the pubsub endpoint - check_delay: the delay between two controller points - multicast_endpoint: the multicast endpoint for circusd cluster auto-discovery """ name = "globaloptions" properties = [] def message(self, *args, **opts): if len(args) > 0: return self.make_message(option=args[0]) else: return self.make_message() def execute(self, arbiter, props): wanted = props.get('option') if wanted: if wanted not in _OPTIONS: raise MessageError('%r not an existing option' % wanted) options = (wanted,) else: options = _OPTIONS res = {} for option in options: res[option] = getattr(arbiter, option) return {"options": res} def console_msg(self, msg): if msg['status'] == "ok": ret = [] for k, v in msg.get('options', {}).items(): ret.append("%s: %s" % (k, convert_opt(k, v))) return "\n".join(ret) return msg['reason'] circus-0.12.1/circus/commands/incrproc.py000066400000000000000000000043231256046442300203550ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError from circus.util import TransformableFuture class IncrProc(Command): """\ Increment the number of processes in a watcher ============================================== This comment increment the number of processes in a watcher by , 1 being the default ZMQ Message ----------- :: { "command": "incr", "properties": { "name": "", "nb": , "waiting": False } } The response return the number of processes in the 'numprocesses` property:: { "status": "ok", "numprocesses": , "time", "timestamp" } Command line ------------ :: $ circusctl incr [] [--waiting] Options +++++++ - : name of the watcher. - : the number of processes to add. """ name = "incr" properties = ['name'] options = Command.waiting_options def message(self, *args, **opts): if len(args) < 1: raise ArgumentError("Invalid number of arguments") options = {'name': args[0]} if len(args) > 1: options['nb'] = int(args[1]) options.update(opts) return self.make_message(**options) def execute(self, arbiter, props): watcher = self._get_watcher(arbiter, props.get('name')) if watcher.singleton: return {"numprocesses": watcher.numprocesses, "singleton": True} else: nb = props.get("nb", 1) resp = TransformableFuture() resp.set_upstream_future(watcher.incr(nb)) resp.set_transform_function(lambda x: {"numprocesses": x}) return resp def console_msg(self, msg): if msg.get("status") == "ok": if "singleton" in msg: return ('This watcher is a Singleton - not changing the number' ' of processes') else: return str(msg.get("numprocesses", "ok")) return self.console_error(msg) circus-0.12.1/circus/commands/ipythonshell.py000066400000000000000000000034441256046442300212630ustar00rootroot00000000000000import os import sys from circus.exc import ArgumentError from circus.commands.base import Command class IPythonShell(Command): """\ Create shell into circusd process ================================= This command is only useful if you have the ipython package installed. Command Line ------------ :: $ circusctl ipython """ name = "ipython" def message(self, *args, **opts): if len(args) > 0: raise ArgumentError("Invalid message") return self.make_message() def execute(self, arbiter, props): shell = 'kernel-%d.json' % os.getpid() msg = None try: from IPython.kernel.zmq.kernelapp import IPKernelApp if not IPKernelApp.initialized(): app = IPKernelApp.instance() app.initialize([]) main = app.kernel.shell._orig_sys_modules_main_mod if main is not None: sys.modules[ app.kernel.shell._orig_sys_modules_main_name ] = main app.kernel.user_module = sys.modules[__name__] app.kernel.user_ns = {'arbiter': arbiter} app.shell.set_completer_frame() app.kernel.start() except Exception as e: shell = False msg = str(e) return {'shell': shell, 'msg': msg} def console_msg(self, msg): if msg['status'] == "ok": shell = msg['shell'] if shell: from IPython import start_ipython start_ipython(['console', '--existing', shell]) return '' else: msg['reason'] = 'Could not start ipython kernel' return self.console_error(msg) circus-0.12.1/circus/commands/list.py000066400000000000000000000040231256046442300175060ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError from circus import logger class List(Command): """\ Get list of watchers or processes in a watcher ============================================== ZMQ Message ----------- To get the list of all the watchers:: { "command": "list", } To get the list of active processes in a watcher:: { "command": "list", "properties": { "name": "nameofwatcher", } } The response return the list asked. the mapping returned can either be 'watchers' or 'pids' depending the request. Command line ------------ :: $ circusctl list [] """ name = "list" def message(self, *args, **opts): if len(args) > 1: raise ArgumentError("Invalid number of arguments") if len(args) == 1: return self.make_message(name=args[0]) else: return self.make_message() def execute(self, arbiter, props): if 'name' in props: watcher = self._get_watcher(arbiter, props['name']) processes = watcher.get_active_processes() status = [(p.pid, p.status) for p in processes] logger.debug('here is the status of the processes %s' % status) return {"pids": [p.pid for p in processes]} else: watchers = sorted(arbiter._watchers_names) return {"watchers": [name for name in watchers]} def console_msg(self, msg): if "pids" in msg: return ",".join([str(process_id) for process_id in msg.get('pids')]) elif 'watchers' in msg: return ",".join([watcher for watcher in msg.get('watchers')]) if 'reason' not in msg: msg['reason'] = "Response doesn't contain 'pids' nor 'watchers'." return self.console_error(msg) circus-0.12.1/circus/commands/listen.py000066400000000000000000000036001256046442300200310ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import MessageError class Listen(Command): """\ Subscribe to a watcher event ============================ ZMQ --- At any moment you can subscribe to a circus event. Circus provides a PUB/SUB feed on which any clients can subscribe. The subscriber endpoint URI is set in the circus.ini configuration file. Events are pubsub topics: - `watcher..reap`: when a process is reaped - `watcher..spawn`: when a process is spawned - `watcher..kill`: when a process is killed - `watcher..updated`: when watcher configuration is updated - `watcher..stop`: when a watcher is stopped - `watcher..start`: when a watcher is started All events messages are in a json struct. Command line ------------ The client has been updated to provide a simple way to listen on the events:: circusctl listen [, ...] Example of result: ++++++++++++++++++ :: $ circusctl listen tcp://127.0.0.1:5556 watcher.refuge.spawn: {u'process_id': 6, u'process_pid': 72976, u'time': 1331681080.985104} watcher.refuge.spawn: {u'process_id': 7, u'process_pid': 72995, u'time': 1331681086.208542} watcher.refuge.spawn: {u'process_id': 8, u'process_pid': 73014, u'time': 1331681091.427005} """ name = "listen" msg_type = "sub" def message(self, *args, **opts): if not args: return [""] return list(args) def execute(self, arbiter, args): raise MessageError("invalid message. use a pub/sub socket") circus-0.12.1/circus/commands/listsockets.py000066400000000000000000000031621256046442300211050ustar00rootroot00000000000000from circus.commands.base import Command import operator class ListSockets(Command): """\ Get the list of sockets ======================= ZMQ Message ----------- To get the list of sockets:: { "command": "listsockets", } The response return a list of json mappings with keys for fd, name, host and port. Command line ------------ :: $ circusctl listsockets """ name = "listsockets" def message(self, *args, **opts): return self.make_message() def execute(self, arbiter, props): def _get_info(socket): sock = {'fd': socket.fileno(), 'name': socket.name, 'backlog': socket.backlog} if socket.host is not None: sock['host'] = socket.host sock['port'] = socket.port else: sock['path'] = socket.path return sock sockets = [_get_info(socket) for socket in arbiter.sockets.values()] sockets.sort(key=operator.itemgetter('fd')) return {"sockets": sockets} def console_msg(self, msg): if 'sockets' in msg: sockets = [] for sock in msg['sockets']: d = "%(fd)d:socket '%(name)s' " if 'path' in sock: d = (d + 'at %(path)s') % sock else: d = (d + 'at %(host)s:%(port)d') % sock sockets.append(d) return "\n".join(sockets) return self.console_error(msg) circus-0.12.1/circus/commands/numprocesses.py000066400000000000000000000032341256046442300212640ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError class NumProcesses(Command): """\ Get the number of processes =========================== Get the number of processes in a watcher or in a arbiter ZMQ Message ----------- :: { "command": "numprocesses", "propeties": { "name": "" } } The response return the number of processes in the 'numprocesses` property:: { "status": "ok", "numprocesses": , "time", "timestamp" } If the property name isn't specified, the sum of all processes managed is returned. Command line ------------ :: $ circusctl numprocesses [] Options +++++++ - : name of the watcher """ name = "numprocesses" def message(self, *args, **opts): if len(args) > 1: raise ArgumentError("message invalid") if len(args) == 1: return self.make_message(name=args[0]) else: return self.make_message() def execute(self, arbiter, props): if 'name' in props: watcher = self._get_watcher(arbiter, props['name']) return { "numprocesses": len(watcher), "watcher_name": props['name'] } else: return {"numprocesses": arbiter.numprocesses()} def console_msg(self, msg): if msg.get("status") == "ok": return str(msg.get("numprocesses")) return self.console_error(msg) circus-0.12.1/circus/commands/numwatchers.py000066400000000000000000000020521256046442300210730ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError class NumWatchers(Command): """\ Get the number of watchers ========================== Get the number of watchers in a arbiter ZMQ Message ----------- :: { "command": "numwatchers", } The response return the number of watchers in the 'numwatchers` property:: { "status": "ok", "numwatchers": , "time", "timestamp" } Command line ------------ :: $ circusctl numwatchers """ name = "numwatchers" def message(self, *args, **opts): if len(args) > 0: raise ArgumentError("Invalid number of arguments") return self.make_message() def execute(self, arbiter, props): return {"numwatchers": arbiter.numwatchers()} def console_msg(self, msg): if msg.get("status") == "ok": return str(msg.get("numwatchers")) return self.console_error(msg) circus-0.12.1/circus/commands/options.py000066400000000000000000000061251256046442300202330ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError from circus.util import convert_opt class Options(Command): """\ Get the value of all options for a watcher ========================================== This command returns all option values for a given watcher. ZMQ Message ----------- :: { "command": "options", "properties": { "name": "nameofwatcher", } } A message contains 1 property: - name: name of watcher The response object has a property ``options`` which is a dictionary of option names and values. eg:: { "status": "ok", "options": { "graceful_timeout": 300, "send_hup": True, ... }, time': 1332202594.754644 } Command line ------------ :: $ circusctl options Options ------- - : name of the watcher Options Keys are: - numprocesses: integer, number of processes - warmup_delay: integer or number, delay to wait between process spawning in seconds - working_dir: string, directory where the process will be executed - uid: string or integer, user ID used to launch the process - gid: string or integer, group ID used to launch the process - send_hup: boolean, if TRU the signal HUP will be used on reload - shell: boolean, will run the command in the shell environment if true - cmd: string, The command line used to launch the process - env: object, define the environnement in which the process will be launch - retry_in: integer or number, time in seconds we wait before we retry to launch the process if the maximum number of attempts has been reach. - max_retry: integer, The maximum of retries loops - graceful_timeout: integer or number, time we wait before we definitely kill a process. - priority: used to sort watchers in the arbiter - singleton: if True, a singleton watcher. - max_age: time a process can live before being restarted - max_age_variance: variable additional time to live, avoids stampeding herd. """ name = "options" properties = ['name'] def message(self, *args, **opts): if len(args) < 1: raise ArgumentError("number of arguments invalid") return self.make_message(name=args[0]) def execute(self, arbiter, props): watcher = self._get_watcher(arbiter, props['name']) return {"options": dict(watcher.options())} def console_msg(self, msg): if msg['status'] == "ok": ret = [] for k, v in msg.get('options', {}).items(): ret.append("%s: %s" % (k, convert_opt(k, v))) return "\n".join(ret) return self.console_error(msg) circus-0.12.1/circus/commands/quit.py000066400000000000000000000020551256046442300175200ustar00rootroot00000000000000from circus.commands.base import Command class Quit(Command): """\ Quit the arbiter immediately ============================ When the arbiter receive this command, the arbiter exit. ZMQ Message ----------- :: { "command": "quit", "waiting": False } The response return the status "ok". If ``waiting`` is False (default), the call will return immediately after calling ``stop_signal`` on each process. If ``waiting`` is True, the call will return only when the stop process is completely ended. Because of the :ref:`graceful_timeout option `, it can take some time. Command line ------------ :: $ circusctl quit [--waiting] """ name = "quit" options = Command.waiting_options def message(self, *args, **opts): return self.make_message(**opts) def execute(self, arbiter, props): return arbiter.stop() circus-0.12.1/circus/commands/reload.py000066400000000000000000000067111256046442300200070ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError from circus.util import TransformableFuture class Reload(Command): """\ Reload the arbiter or a watcher =============================== This command reloads all the process in a watcher or all watchers. This will happen in one of 3 ways: * If graceful is false, a simple restart occurs. * If `send_hup` is true for the watcher, a HUP signal is sent to each process. * Otherwise: * If sequential is false, the arbiter will attempt to spawn `numprocesses` new processes. If the new processes are spawned successfully, the result is that all of the old processes are stopped, since by default the oldest processes are stopped when the actual number of processes for a watcher is greater than `numprocesses`. * If sequential is true, the arbiter will restart each process in a sequential way (with a `warmup_delay` pause between each step) ZMQ Message ----------- :: { "command": "reload", "properties": { "name": '", "graceful": true, "sequential": false, "waiting": False } } The response return the status "ok". If the property graceful is set to true the processes will be exited gracefully. If the property name is present, then the reload will be applied to the watcher. Command line ------------ :: $ circusctl reload [] [--terminate] [--waiting] [--sequential] Options +++++++ - : name of the watcher - --terminate; quit the node immediately """ name = "reload" options = (Command.options + Command.waiting_options + [('', 'sequential', False, "sequential reload")] + [('', 'terminate', False, "stop immediately")]) def message(self, *args, **opts): if len(args) > 1: raise ArgumentError("invalid number of arguments") graceful = not opts.get("terminate", False) waiting = opts.get("waiting", False) sequential = opts.get("sequential", False) if len(args) == 1: return self.make_message(name=args[0], graceful=graceful, waiting=waiting, sequential=sequential) else: return self.make_message(graceful=graceful, waiting=waiting, sequential=sequential) def execute(self, arbiter, props): graceful = props.get('graceful', True) sequential = props.get('sequential', False) if 'name' in props: watcher = self._get_watcher(arbiter, props['name']) if props.get('waiting'): resp = TransformableFuture() resp.set_upstream_future(watcher.reload( graceful=graceful, sequential=sequential)) resp.set_transform_function(lambda x: {"info": x}) return resp return watcher.reload(graceful=graceful, sequential=sequential) else: return arbiter.reload(graceful=graceful, sequential=sequential) circus-0.12.1/circus/commands/reloadconfig.py000066400000000000000000000016671256046442300212020ustar00rootroot00000000000000from circus.commands.base import Command class ReloadConfig(Command): """\ Reload the configuration file ============================= This command reloads the configuration file, so changes in the configuration file will be reflected in the configuration of circus. ZMQ Message ----------- :: { "command": "reloadconfig", "waiting": False } The response return the status "ok". If the property graceful is set to true the processes will be exited gracefully. Command line ------------ :: $ circusctl reloadconfig [--waiting] """ name = "reloadconfig" options = Command.waiting_options def message(self, *args, **opts): return self.make_message(**opts) def execute(self, arbiter, props): return arbiter.reload_from_config() circus-0.12.1/circus/commands/restart.py000066400000000000000000000100141256046442300202140ustar00rootroot00000000000000import re import fnmatch from functools import partial from circus.commands.base import Command from circus.exc import ArgumentError, MessageError from circus.util import TransformableFuture def execute_watcher_start_stop_restart(command, arbiter, props, watcher_function_name, watchers_function, arbiter_function): """base function to handle start/stop/restart watcher requests. since this is always the same procedure except some function names this function handles all watcher start/stop commands """ if 'name' in props: match = props.get('match', 'glob') if match == 'simple': watchers = [command._get_watcher(arbiter, props['name'])] else: if match == 'glob': name = re.compile(fnmatch.translate(props['name'])) elif match == 'regex': name = re.compile(props['name']) else: raise MessageError("unknown match method %s" % match) watchers = [watcher for watcher in arbiter.iter_watchers() if name.match(watcher.name.lower())] if not watchers: raise MessageError("program %s not found" % props['name']) if len(watchers) == 1: if props.get('waiting'): resp = TransformableFuture() func = getattr(watchers[0], watcher_function_name) resp.set_upstream_future(func()) resp.set_transform_function(lambda x: {"info": x}) return resp return getattr(watchers[0], watcher_function_name)() def watcher_iter_func(reverse=True): return sorted(watchers, key=lambda a: a.priority, reverse=reverse) return watchers_function(watcher_iter_func=watcher_iter_func) else: return arbiter_function() match_options = ('match', 'match', 'glob', "Watcher name matching method (simple, glob or regex)") class Restart(Command): """\ Restart the arbiter or a watcher ================================ This command restart all the process in a watcher or all watchers. This funtion simply stop a watcher then restart it. ZMQ Message ----------- :: { "command": "restart", "properties": { "name": "", "waiting": False, "match": "[simple|glob|regex]" } } The response return the status "ok". If the property name is present, then the reload will be applied to the watcher. If ``waiting`` is False (default), the call will return immediately after calling `stop_signal` on each process. If ``waiting`` is True, the call will return only when the restart process is completely ended. Because of the :ref:`graceful_timeout option `, it can take some time. The ``match`` parameter can have the value ``simple`` for string compare, ``glob`` for wildcard matching (default) or ``regex`` for regex matching. Command line ------------ :: $ circusctl restart [name] [--waiting] [--match=simple|glob|regex] Options +++++++ - : name or pattern of the watcher(s) - : watcher match method """ name = "restart" options = list(Command.waiting_options) options.append(match_options) def message(self, *args, **opts): if len(args) > 1: raise ArgumentError("Invalid number of arguments") if len(args) == 1: return self.make_message(name=args[0], **opts) return self.make_message(**opts) def execute(self, arbiter, props): return execute_watcher_start_stop_restart( self, arbiter, props, 'restart', arbiter.restart, partial(arbiter.restart, inside_circusd=True)) circus-0.12.1/circus/commands/rmwatcher.py000066400000000000000000000041431256046442300205320ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError class RmWatcher(Command): """\ Remove a watcher ================ This command removes a watcher dynamically from the arbiter. The watchers are gracefully stopped by default. ZMQ Message ----------- :: { "command": "rm", "properties": { "name": "", "nostop": False, "waiting": False } } The response return a status "ok". If ``nostop`` is True (default: False), the processes for the watcher will not be stopped - instead the watcher will just be forgotten by circus and the watcher processes will be responsible for stopping themselves. If ``nostop`` is not specified or is False, then the watcher processes will be stopped gracefully. If ``waiting`` is False (default), the call will return immediately after starting to remove and stop the corresponding watcher. If ``waiting`` is True, the call will return only when the remove and stop process is completely ended. Because of the :ref:`graceful_timeout option `, it can take some time. Command line ------------ :: $ circusctl rm [--waiting] [--nostop] Options +++++++ - : name of the watcher to remove - nostop: do not stop the watcher processes, just remove the watcher """ name = "rm" properties = ['name'] options = Command.waiting_options + \ [('nostop', 'nostop', False, 'Do not stop watcher processes')] def message(self, *args, **opts): if len(args) < 1 or len(args) > 1: raise ArgumentError("Invalid number of arguments") return self.make_message(name=args[0]) def execute(self, arbiter, props): self._get_watcher(arbiter, props['name']) return arbiter.rm_watcher(props['name'], props.get('nostop', False)) circus-0.12.1/circus/commands/sendsignal.py000066400000000000000000000107641256046442300206730ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError, MessageError from circus.util import to_signum class Signal(Command): """\ Send a signal ============= This command allows you to send a signal to all processes in a watcher, a specific process in a watcher or its children. ZMQ Message ----------- To send a signal to all the processes for a watcher:: { "command": "signal", "property": { "name": , "signum": } To send a signal to a process:: { "command": "signal", "property": { "name": , "pid": , "signum": } An optional property "children" can be used to send the signal to all the children rather than the process itself:: { "command": "signal", "property": { "name": , "pid": , "signum": , "children": True } To send a signal to a process child:: { "command": "signal", "property": { "name": , "pid": , "signum": , "child_pid": , } It is also possible to send a signal to all the children of the watcher:: { "command": "signal", "property": { "name": , "signum": , "children": True } Lastly, you can send a signal to the process *and* its children, with the *recursive* option:: { "command": "signal", "property": { "name": , "signum": , "recursive": True } Command line ------------ :: $ circusctl signal [] [--children] [--recursive] Options: ++++++++ - : the name of the watcher - : integer, the process id. - : the signal number (or name) to send. - : the pid of a child, if any - : boolean, send the signal to all the children - : boolean, send the signal to the process and its children """ name = "signal" options = [('', 'children', False, "Only signal children of the process"), ('', 'recursive', False, "Signal parent and children")] properties = ['name', 'signum'] def message(self, *args, **opts): largs = len(args) if largs < 2 or largs > 3: raise ArgumentError("Invalid number of arguments") props = { 'name': args[0], 'children': opts.get("children", False), 'recursive': opts.get("recursive", False), 'signum': args[-1], } if len(args) == 3: props['pid'] = int(args[1]) return self.make_message(**props) def execute(self, arbiter, props): name = props.get('name') watcher = self._get_watcher(arbiter, name) signum = props.get('signum') pids = [props['pid']] if 'pid' in props else watcher.get_active_pids() childpid = props.get('childpid', None) children = props.get('children', False) recursive = props.get('recursive', False) for pid in pids: if childpid: watcher.send_signal_child(pid, childpid, signum) elif children: watcher.send_signal_children(pid, signum) else: # send to the given pid watcher.send_signal(pid, signum) if recursive: # also send to the children watcher.send_signal_children(pid, signum, recursive=True) def validate(self, props): super(Signal, self).validate(props) if 'childpid' in props and 'pid' not in props: raise ArgumentError('cannot specify childpid without pid') try: props['signum'] = to_signum(props['signum']) except ValueError: raise MessageError('signal invalid') circus-0.12.1/circus/commands/set.py000066400000000000000000000050721256046442300173330ustar00rootroot00000000000000from circus.commands.base import Command from circus.commands.util import convert_option, validate_option from circus.exc import ArgumentError, MessageError class Set(Command): """\ Set a watcher option ==================== ZMQ Message ----------- :: { "command": "set", "properties": { "name": "nameofwatcher", "options": { "key1": "val1", .. } "waiting": False } } The response return the status "ok". See the command Options for a list of key to set. Command line ------------ :: $ circusctl set --waiting """ name = "set" properties = ['name', 'options'] options = Command.waiting_options def message(self, *args, **opts): if len(args) < 3: raise ArgumentError("Invalid number of arguments") args = list(args) watcher_name = args.pop(0) if len(args) % 2 != 0: raise ArgumentError("List of key/values is invalid") options = {} while len(args) > 0: kv, args = args[:2], args[2:] kvl = kv[0].lower() options[kvl] = convert_option(kvl, kv[1]) if opts.get('waiting', False): return self.make_message(name=watcher_name, waiting=True, options=options) else: return self.make_message(name=watcher_name, options=options) def execute(self, arbiter, props): watcher = self._get_watcher(arbiter, props.pop('name')) action = 0 for key, val in props.get('options', {}).items(): if key == 'hooks': new_action = 0 for name, _val in val.items(): action = watcher.set_opt('hooks.%s' % name, _val) if action == 1: new_action = 1 else: new_action = watcher.set_opt(key, val) if new_action == 1: action = 1 # trigger needed action return watcher.do_action(action) def validate(self, props): super(Set, self).validate(props) options = props['options'] if not isinstance(options, dict): raise MessageError("'options' property should be an object") for key, val in options.items(): validate_option(key, val) circus-0.12.1/circus/commands/start.py000066400000000000000000000041201256046442300176660ustar00rootroot00000000000000from circus.commands.base import Command from circus.commands.restart import execute_watcher_start_stop_restart from circus.commands.restart import match_options from circus.exc import ArgumentError class Start(Command): """\ Start the arbiter or a watcher ============================== This command starts all the processes in a watcher or all watchers. ZMQ Message ----------- :: { "command": "start", "properties": { "name": '", "waiting": False, "match": "[simple|glob|regex]" } } The response return the status "ok". If the property name is present, the watcher will be started. If ``waiting`` is False (default), the call will return immediately after calling `start` on each process. If ``waiting`` is True, the call will return only when the start process is completely ended. Because of the :ref:`graceful_timeout option `, it can take some time. The ``match`` parameter can have the value ``simple`` for string compare, ``glob`` for wildcard matching (default) or ``regex`` for regex matching. Command line ------------ :: $ circusctl restart [name] [--waiting] [--match=simple|glob|regex] Options +++++++ - : name or pattern of the watcher(s) - : watcher match method """ name = "start" options = list(Command.waiting_options) options.append(match_options) def message(self, *args, **opts): if len(args) > 1: raise ArgumentError("Invalid number of arguments") if len(args) == 1: return self.make_message(name=args[0], **opts) return self.make_message(**opts) def execute(self, arbiter, props): return execute_watcher_start_stop_restart( self, arbiter, props, 'start', arbiter.start_watchers, arbiter.start_watchers) circus-0.12.1/circus/commands/stats.py000066400000000000000000000112731256046442300176760ustar00rootroot00000000000000from circus.exc import MessageError, ArgumentError from circus.commands.base import Command _INFOLINE = ("%(pid)s %(cmdline)s %(username)s %(nice)s %(mem_info1)s " "%(mem_info2)s %(cpu)s %(mem)s %(ctime)s") class Stats(Command): """\ Get process infos ================= You can get at any time some statistics about your processes with the stat command. ZMQ Message ----------- To get stats for all watchers:: { "command": "stats" } To get stats for a watcher:: { "command": "stats", "properties": { "name": } } To get stats for a process:: { "command": "stats", "properties": { "name": , "process": } } Stats can be extended with the extended_stats hook but extended stats need to be requested:: { "command": "stats", "properties": { "name": , "process": , "extended": True } } The response retun an object per process with the property "info" containing some process informations:: { "info": { "children": [], "cmdline": "python", "cpu": 0.1, "ctime": "0:00.41", "mem": 0.1, "mem_info1": "3M", "mem_info2": "2G", "nice": 0, "pid": 47864, "username": "root" }, "process": 5, "status": "ok", "time": 1332265655.897085 } Command Line ------------ :: $ circusctl stats [--extended] [] [] """ name = "stats" options = [('', 'extended', False, "Include info from extended_stats hook")] def message(self, *args, **opts): if len(args) > 2: raise ArgumentError("message invalid") extended = opts.get("extended", False) if len(args) == 2: return self.make_message(name=args[0], process=int(args[1]), extended=extended) elif len(args) == 1: return self.make_message(name=args[0], extended=extended) else: return self.make_message(extended=extended) def execute(self, arbiter, props): if 'name' in props: watcher = self._get_watcher(arbiter, props['name']) if 'process' in props: try: return { "process": props['process'], "info": watcher.process_info(props['process'], props.get('extended')), } except KeyError: raise MessageError("process %r not found in %r" % ( props['process'], props['name'])) else: return {"name": props['name'], "info": watcher.info(props.get('extended'))} else: infos = {} for watcher in arbiter.watchers: infos[watcher.name] = watcher.info() return {"infos": infos} def _to_str(self, info): if isinstance(info, dict): children = info.pop("children", []) ret = [_INFOLINE % info] for child in children: ret.append(" " + _INFOLINE % child) return "\n".join(ret) else: # basestring, int, .. return info def console_msg(self, msg): if msg['status'] == "ok": if "name" in msg: ret = ["%s:" % msg.get('name')] for process, info in msg.get('info', {}).items(): ret.append("%s: %s" % (process, self._to_str(info))) return "\n".join(ret) elif 'infos' in msg: ret = [] for watcher, watcher_info in msg.get('infos', {}).items(): ret.append("%s:" % watcher) watcher_info = watcher_info or {} for process, info in watcher_info.items(): ret.append("%s: %s" % (process, self._to_str(info))) return "\n".join(ret) else: return "%s: %s\n" % (msg['process'], self._to_str(msg['info'])) else: return self.console_error(msg) circus-0.12.1/circus/commands/status.py000066400000000000000000000035541256046442300200660ustar00rootroot00000000000000from circus.commands.base import Command from circus.exc import ArgumentError class Status(Command): """\ Get the status of a watcher or all watchers =========================================== This command start get the status of a watcher or all watchers. ZMQ Message ----------- :: { "command": "status", "properties": { "name": '", } } The response return the status "active" or "stopped" or the status / watchers. Command line ------------ :: $ circusctl status [] Options +++++++ - : name of the watcher Example +++++++ :: $ circusctl status dummy active $ circusctl status dummy: active dummy2: active refuge: active """ name = "status" def message(self, *args, **opts): if len(args) > 1: raise ArgumentError("message invalid") if len(args) == 1: return self.make_message(name=args[0]) else: return self.make_message() def execute(self, arbiter, props): if 'name' in props: watcher = self._get_watcher(arbiter, props['name']) return {"status": watcher.status()} else: return {"statuses": arbiter.statuses()} def console_msg(self, msg): if "statuses" in msg: statuses = msg.get("statuses") watchers = sorted(statuses) return "\n".join(["%s: %s" % (watcher, statuses[watcher]) for watcher in watchers]) elif "status" in msg and "status" != "error": return msg.get("status") return self.console_error(msg) circus-0.12.1/circus/commands/stop.py000066400000000000000000000040061256046442300175210ustar00rootroot00000000000000from circus.commands.base import Command from circus.commands.restart import execute_watcher_start_stop_restart from circus.commands.restart import match_options class Stop(Command): """\ Stop watchers ============= This command stops a given watcher or all watchers. ZMQ Message ----------- :: { "command": "stop", "properties": { "name": "", "waiting": False, "match": "[simple|glob|regex]" } } The response returns the status "ok". If the ``name`` property is present, then the stop will be applied to the watcher corresponding to that name. Otherwise, all watchers will get stopped. If ``waiting`` is False (default), the call will return immediatly after calling `stop_signal` on each process. If ``waiting`` is True, the call will return only when the stop process is completly ended. Because of the :ref:`graceful_timeout option `, it can take some time. The ``match`` parameter can have the value ``simple`` for string compare, ``glob`` for wildcard matching (default) or ``regex`` for regex matching. Command line ------------ :: $ circusctl stop [name] [--waiting] [--match=simple|glob|regex] Options +++++++ - : name or pattern of the watcher(s) - : watcher match method """ name = "stop" options = list(Command.waiting_options) options.append(match_options) def message(self, *args, **opts): if len(args) >= 1: return self.make_message(name=args[0], **opts) return self.make_message(**opts) def execute(self, arbiter, props): return execute_watcher_start_stop_restart( self, arbiter, props, 'stop', arbiter.stop_watchers, arbiter.stop_watchers) circus-0.12.1/circus/commands/util.py000066400000000000000000000132321256046442300175120ustar00rootroot00000000000000from circus.exc import ArgumentError, MessageError from circus.py3compat import string_types from circus import util import warnings try: import resource except ImportError: resource = None # NOQA _HOOKS = ('before_start', 'after_start', 'before_stop', 'after_stop', 'before_spawn', 'after_spawn', 'before_signal', 'after_signal', 'extended_stats') def convert_option(key, val): if key == "numprocesses": return int(val) elif key == "warmup_delay": return float(val) elif key == "working_dir": return val elif key == "uid": return val elif key == "gid": return val elif key == "send_hup": return util.to_bool(val) elif key == "stop_signal": return util.to_signum(val) elif key == "stop_children": return util.to_bool(val) elif key == "shell": return util.to_bool(val) elif key == "copy_env": return util.to_bool(val) elif key == "env": return util.parse_env_dict(val) elif key == "cmd": return val elif key == "args": return val elif key == "retry_in": return float(val) elif key == "max_retry": return int(val) elif key == "graceful_timeout": return float(val) elif key == 'max_age': return int(val) elif key == 'max_age_variance': return int(val) elif key == 'respawn': return util.to_bool(val) elif key == "singleton": return util.to_bool(val) elif key == "close_child_stdout": return util.to_bool(val) elif key == "close_child_stderr": return util.to_bool(val) elif key.startswith('stderr_stream.') or key.startswith('stdout_stream.'): subkey = key.split('.', 1)[-1] if subkey in ('max_bytes', 'backup_count'): return int(val) return val elif key == 'hooks': res = {} for hook in val.split(','): if hook == '': continue hook = hook.split(':') if len(hook) != 2: raise ArgumentError(hook) name, value = hook if name not in _HOOKS: raise ArgumentError(name) res[name] = value return res elif key.startswith('hooks.'): # we can also set a single hook name = key.split('.', 1)[-1] if name not in _HOOKS: raise ArgumentError(name) return val elif key.startswith('rlimit_'): return int(val) raise ArgumentError("unknown key %r" % key) def validate_option(key, val): valid_keys = ('numprocesses', 'warmup_delay', 'working_dir', 'uid', 'gid', 'send_hup', 'stop_signal', 'stop_children', 'shell', 'env', 'cmd', 'args', 'copy_env', 'retry_in', 'max_retry', 'graceful_timeout', 'stdout_stream', 'stderr_stream', 'max_age', 'max_age_variance', 'respawn', 'singleton', 'hooks', 'close_child_stdout', 'close_child_stderr') valid_prefixes = ('stdout_stream.', 'stderr_stream.', 'hooks.', 'rlimit_') def _valid_prefix(): for prefix in valid_prefixes: if key.startswith('%s' % prefix): return True return False if key not in valid_keys and not _valid_prefix(): raise MessageError('unknown key %r' % key) if key in ('numprocesses', 'max_retry', 'max_age', 'max_age_variance', 'stop_signal'): if not isinstance(val, int): raise MessageError("%r isn't an integer" % key) elif key in ('warmup_delay', 'retry_in', 'graceful_timeout',): if not isinstance(val, (int, float)): raise MessageError("%r isn't a number" % key) elif key in ('uid', 'gid',): if not isinstance(val, int) and not isinstance(val, string_types): raise MessageError("%r isn't an integer or string" % key) elif key in ('send_hup', 'shell', 'copy_env', 'respawn', 'stop_children', 'close_child_stdout', 'close_child_stderr'): if not isinstance(val, bool): raise MessageError("%r isn't a valid boolean" % key) elif key in ('env', ): if not isinstance(val, dict): raise MessageError("%r isn't a valid object" % key) for k, v in val.items(): if not isinstance(v, string_types): raise MessageError("%r isn't a string" % k) elif key == 'hooks': if not isinstance(val, dict): raise MessageError("%r isn't a valid hook dict" % val) for key in val: if key not in _HOOKS: raise MessageError("Unknown hook %r" % val) elif key in ('stderr_stream', 'stdout_stream'): if not isinstance(val, dict): raise MessageError("%r isn't a valid object" % key) if 'class' not in val: raise MessageError("%r must have a 'class' key" % key) if 'refresh_time' in val: warnings.warn("'refresh_time' is deprecated and not useful " "anymore for %r" % key) elif key.startswith('rlimit_'): if resource: rlimit_key = key[7:] rlimit_int = getattr( resource, 'RLIMIT_' + rlimit_key.upper(), None ) if rlimit_int is None: raise MessageError("%r isn't a valid rlimit setting" % key) else: raise MessageError("rlimit options are not supported on this" " platform") # note that a null val means RLIM_INFINITY if val is not None and not isinstance(val, int): raise MessageError("%r rlimit value isn't a valid int" % val) circus-0.12.1/circus/config.py000066400000000000000000000271341256046442300162070ustar00rootroot00000000000000import glob import os import signal import warnings from fnmatch import fnmatch try: import resource except ImportError: resource = None # NOQA from circus import logger from circus.py3compat import sort_by_field from circus.util import (DEFAULT_ENDPOINT_DEALER, DEFAULT_ENDPOINT_SUB, DEFAULT_ENDPOINT_MULTICAST, DEFAULT_ENDPOINT_STATS, StrictConfigParser, replace_gnu_args, to_signum, to_bool, papa) def watcher_defaults(): return { 'name': '', 'cmd': '', 'args': '', 'numprocesses': 1, 'warmup_delay': 0, 'executable': None, 'working_dir': None, 'shell': False, 'uid': None, 'gid': None, 'send_hup': False, 'stop_signal': signal.SIGTERM, 'stop_children': False, 'max_retry': 5, 'graceful_timeout': 30, 'rlimits': dict(), 'stderr_stream': dict(), 'stdout_stream': dict(), 'priority': 0, 'use_sockets': False, 'singleton': False, 'copy_env': False, 'copy_path': False, 'hooks': dict(), 'respawn': True, 'autostart': True, 'use_papa': False} class DefaultConfigParser(StrictConfigParser): def __init__(self, *args, **kw): StrictConfigParser.__init__(self, *args, **kw) self._env = dict(os.environ) def set_env(self, env): self._env = dict(env) def get(self, section, option): res = StrictConfigParser.get(self, section, option) return replace_gnu_args(res, env=self._env) def items(self, section, noreplace=False): items = StrictConfigParser.items(self, section) if noreplace: return items return [(key, replace_gnu_args(value, env=self._env)) for key, value in items] def dget(self, section, option, default=None, type=str): if not self.has_option(section, option): return default value = self.get(section, option) if type is int: value = int(value) elif type is bool: value = to_bool(value) elif type is float: value = float(value) elif type is not str: raise NotImplementedError() return value def rlimit_value(val): if resource is not None and (val is None or len(val) == 0): return resource.RLIM_INFINITY else: return int(val) def read_config(config_path): cfg = DefaultConfigParser() with open(config_path) as f: if hasattr(cfg, 'read_file'): cfg.read_file(f) else: cfg.readfp(f) current_dir = os.path.dirname(config_path) # load included config files includes = [] def _scan(filename, includes): if os.path.abspath(filename) != filename: filename = os.path.join(current_dir, filename) paths = glob.glob(filename) if paths == []: logger.warn('%r does not lead to any config. Make sure ' 'include paths are relative to the main config ' 'file' % filename) includes += paths for include_file in cfg.dget('circus', 'include', '').split(): _scan(include_file, includes) for include_dir in cfg.dget('circus', 'include_dir', '').split(): _scan(os.path.join(include_dir, '*.ini'), includes) logger.debug('Reading config files: %s' % includes) return cfg, [config_path] + cfg.read(includes) def get_config(config_file): if not os.path.exists(config_file): raise IOError("the configuration file %r does not exist\n" % config_file) cfg, cfg_files_read = read_config(config_file) dget = cfg.dget config = {} # reading the global environ first global_env = dict(os.environ.items()) local_env = dict() # update environments with [env] section if 'env' in cfg.sections(): local_env.update(dict(cfg.items('env'))) global_env.update(local_env) # always set the cfg environment cfg.set_env(global_env) # main circus options config['check_delay'] = dget('circus', 'check_delay', 5., float) config['endpoint'] = dget('circus', 'endpoint', DEFAULT_ENDPOINT_DEALER) config['endpoint_owner'] = dget('circus', 'endpoint_owner', None, str) config['pubsub_endpoint'] = dget('circus', 'pubsub_endpoint', DEFAULT_ENDPOINT_SUB) config['multicast_endpoint'] = dget('circus', 'multicast_endpoint', DEFAULT_ENDPOINT_MULTICAST) config['stats_endpoint'] = dget('circus', 'stats_endpoint', None) config['statsd'] = dget('circus', 'statsd', False, bool) config['umask'] = dget('circus', 'umask', None) if config['umask']: config['umask'] = int(config['umask'], 8) if config['stats_endpoint'] is None: config['stats_endpoint'] = DEFAULT_ENDPOINT_STATS elif not config['statsd']: warnings.warn("You defined a stats_endpoint without " "setting up statsd to True.", DeprecationWarning) config['statsd'] = True config['warmup_delay'] = dget('circus', 'warmup_delay', 0, int) config['httpd'] = dget('circus', 'httpd', False, bool) config['httpd_host'] = dget('circus', 'httpd_host', 'localhost', str) config['httpd_port'] = dget('circus', 'httpd_port', 8080, int) config['debug'] = dget('circus', 'debug', False, bool) config['debug_gc'] = dget('circus', 'debug_gc', False, bool) config['pidfile'] = dget('circus', 'pidfile') config['loglevel'] = dget('circus', 'loglevel') config['logoutput'] = dget('circus', 'logoutput') config['loggerconfig'] = dget('circus', 'loggerconfig', None) config['fqdn_prefix'] = dget('circus', 'fqdn_prefix', None, str) config['papa_endpoint'] = dget('circus', 'fqdn_prefix', None, str) # Initialize watchers, plugins & sockets to manage watchers = [] plugins = [] sockets = [] for section in cfg.sections(): if section.startswith("socket:"): sock = dict(cfg.items(section)) sock['name'] = section.split("socket:")[-1].lower() sock['so_reuseport'] = dget(section, "so_reuseport", False, bool) sock['replace'] = dget(section, "replace", False, bool) sockets.append(sock) if section.startswith("plugin:"): plugin = dict(cfg.items(section)) plugin['name'] = section if 'priority' in plugin: plugin['priority'] = int(plugin['priority']) plugins.append(plugin) if section.startswith("watcher:"): watcher = watcher_defaults() watcher['name'] = section.split("watcher:", 1)[1] # create watcher options for opt, val in cfg.items(section, noreplace=True): if opt in ('cmd', 'args', 'working_dir', 'uid', 'gid'): watcher[opt] = val elif opt == 'numprocesses': watcher['numprocesses'] = dget(section, 'numprocesses', 1, int) elif opt == 'warmup_delay': watcher['warmup_delay'] = dget(section, 'warmup_delay', 0, int) elif opt == 'executable': watcher['executable'] = dget(section, 'executable', None, str) # default bool to False elif opt in ('shell', 'send_hup', 'stop_children', 'close_child_stderr', 'use_sockets', 'singleton', 'copy_env', 'copy_path', 'close_child_stdout'): watcher[opt] = dget(section, opt, False, bool) elif opt == 'stop_signal': watcher['stop_signal'] = to_signum(val) elif opt == 'max_retry': watcher['max_retry'] = dget(section, "max_retry", 5, int) elif opt == 'graceful_timeout': watcher['graceful_timeout'] = dget( section, "graceful_timeout", 30, int) elif opt.startswith('stderr_stream') or \ opt.startswith('stdout_stream'): stream_name, stream_opt = opt.split(".", 1) watcher[stream_name][stream_opt] = val elif opt.startswith('rlimit_'): limit = opt[7:] watcher['rlimits'][limit] = rlimit_value(val) elif opt == 'priority': watcher['priority'] = dget(section, "priority", 0, int) elif opt == 'use_papa' and dget(section, 'use_papa', False, bool): if papa: watcher['use_papa'] = True else: warnings.warn("Config file says use_papa but the papa " "module is missing.", ImportWarning) elif opt.startswith('hooks.'): hook_name = opt[len('hooks.'):] val = [elmt.strip() for elmt in val.split(',', 1)] if len(val) == 1: val.append(False) else: val[1] = to_bool(val[1]) watcher['hooks'][hook_name] = val # default bool to True elif opt in ('check_flapping', 'respawn', 'autostart'): watcher[opt] = dget(section, opt, True, bool) else: # freeform watcher[opt] = val if watcher['copy_env']: watcher['env'] = dict(global_env) else: watcher['env'] = dict(local_env) watchers.append(watcher) # making sure we return consistent lists sort_by_field(watchers) sort_by_field(plugins) sort_by_field(sockets) # Second pass to make sure env sections apply to all watchers. def _extend(target, source): for name, value in source: if name in target: continue target[name] = value def _expand_vars(target, key, env): if isinstance(target[key], str): target[key] = replace_gnu_args(target[key], env=env) elif isinstance(target[key], dict): for k in target[key].keys(): _expand_vars(target[key], k, env) def _expand_section(section, env, exclude=None): if exclude is None: exclude = ('name', 'env') for option in section.keys(): if option in exclude: continue _expand_vars(section, option, env) # build environment for watcher sections for section in cfg.sections(): if section.startswith('env:'): section_elements = section.split("env:", 1)[1] watcher_patterns = [s.strip() for s in section_elements.split(',')] env_items = dict(cfg.items(section, noreplace=True)) for pattern in watcher_patterns: match = [w for w in watchers if fnmatch(w['name'], pattern)] for watcher in match: watcher['env'].update(env_items) # expand environment for watcher sections for watcher in watchers: env = dict(global_env) env.update(watcher['env']) _expand_section(watcher, env) config['watchers'] = watchers config['plugins'] = plugins config['sockets'] = sockets return config circus-0.12.1/circus/consumer.py000066400000000000000000000036471256046442300166000ustar00rootroot00000000000000import errno import zmq from circus.util import DEFAULT_ENDPOINT_SUB, get_connection from circus.py3compat import b class CircusConsumer(object): def __init__(self, topics, context=None, endpoint=DEFAULT_ENDPOINT_SUB, ssh_server=None, timeout=1.): self.topics = topics self.keep_context = context is not None self._init_context(context) self.endpoint = endpoint self.pubsub_socket = self.context.socket(zmq.SUB) get_connection(self.pubsub_socket, self.endpoint, ssh_server) for topic in self.topics: self.pubsub_socket.setsockopt(zmq.SUBSCRIBE, b(topic)) self._init_poller() self.timeout = timeout def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): """ On context manager exit, destroy the zmq context """ self.stop() def __iter__(self): return self.iter_messages() def _init_context(self, context): self.context = context or zmq.Context() def _init_poller(self): self.poller = zmq.Poller() self.poller.register(self.pubsub_socket, zmq.POLLIN) def iter_messages(self): """ Yields tuples of (topic, message) """ with self: while True: try: events = dict(self.poller.poll(self.timeout * 1000)) except zmq.ZMQError as e: if e.errno == errno.EINTR: continue raise if len(events) == 0: continue topic, message = self.pubsub_socket.recv_multipart() yield topic, message def stop(self): if self.keep_context: return try: self.context.destroy(0) except zmq.ZMQError as e: if e.errno == errno.EINTR: pass else: raise circus-0.12.1/circus/controller.py000066400000000000000000000236131256046442300171230ustar00rootroot00000000000000import os import sys import traceback import functools try: from queue import Queue, Empty from urllib.parse import urlparse except ImportError: from Queue import Queue, Empty # NOQA from urlparse import urlparse # NOQA import zmq import zmq.utils.jsonapi as json from zmq.eventloop import ioloop, zmqstream from tornado.concurrent import Future from circus.util import create_udp_socket from circus.util import check_future_exception_and_log from circus.util import to_uid from circus.commands import get_commands, ok, error, errors from circus import logger from circus.exc import MessageError, ConflictError from circus.py3compat import string_types from circus.sighandler import SysHandler class Controller(object): def __init__(self, endpoint, multicast_endpoint, context, loop, arbiter, check_delay=1.0, endpoint_owner=None): self.arbiter = arbiter self.caller = None self.endpoint = endpoint self.multicast_endpoint = multicast_endpoint self.context = context self.loop = loop self.check_delay = check_delay * 1000 self.endpoint_owner = endpoint_owner self.started = False self._managing_watchers_future = None # initialize the sys handler self._init_syshandler() # get registered commands self.commands = get_commands() def _init_syshandler(self): self.sys_hdl = SysHandler(self) def _init_stream(self): self.stream = zmqstream.ZMQStream(self.ctrl_socket, self.loop) self.stream.on_recv(self.handle_message) def _init_multicast_endpoint(self): multicast_addr, multicast_port = urlparse(self.multicast_endpoint)\ .netloc.split(':') try: self.udp_socket = create_udp_socket(multicast_addr, multicast_port) self.loop.add_handler(self.udp_socket.fileno(), self.handle_autodiscover_message, ioloop.IOLoop.READ) except (IOError, OSError, ValueError): message = ("Multicast discovery is disabled, there was an" "error during udp socket creation.") logger.warning(message, exc_info=True) @property def endpoint_owner_mode(self): return self.endpoint_owner is not None and \ self.endpoint.startswith('ipc://') def initialize(self): # initialize controller # Initialize ZMQ Sockets self.ctrl_socket = self.context.socket(zmq.ROUTER) self.ctrl_socket.bind(self.endpoint) self.ctrl_socket.linger = 0 # support chown'ing the zmq endpoint on unix platforms if self.endpoint_owner_mode: uid = to_uid(self.endpoint_owner) sockpath = self.endpoint[6:] # length of 'ipc://' prefix os.chown(sockpath, uid, -1) self._init_stream() # Initialize UDP Socket if self.multicast_endpoint: self._init_multicast_endpoint() def manage_watchers(self): if self._managing_watchers_future is not None: logger.debug("manage_watchers is already running...") return try: self._managing_watchers_future = self.arbiter.manage_watchers() self.loop.add_future(self._managing_watchers_future, self._manage_watchers_cb) except ConflictError: logger.debug("manage_watchers is conflicting with another command") def _manage_watchers_cb(self, future): self._managing_watchers_future = None def start(self): self.initialize() if self.check_delay > 0: # The specific case (check_delay < 0) # so with no period callback to manage_watchers # is probably "unit tests only" self.caller = ioloop.PeriodicCallback(self.manage_watchers, self.check_delay, self.loop) self.caller.start() self.started = True def stop(self): if self.started: if self.caller is not None: self.caller.stop() try: self.stream.flush() self.stream.close() except (IOError, zmq.ZMQError): pass self.ctrl_socket.close() self.sys_hdl.stop() def handle_message(self, raw_msg): cid, msg = raw_msg msg = msg.strip() if not msg: self.send_response(None, cid, msg, "error: empty command") else: logger.debug("got message %s", msg) self.dispatch((cid, msg)) def handle_autodiscover_message(self, fd_no, type): __, address = self.udp_socket.recvfrom(1024) self.udp_socket.sendto(json.dumps({'endpoint': self.endpoint}), address) def _dispatch_callback_future(self, msg, cid, mid, cast, cmd_name, send_resp, future): exception = check_future_exception_and_log(future) if exception is not None: if send_resp: self.send_error(mid, cid, msg, "server error", cast=cast, errno=errors.BAD_MSG_DATA_ERROR) else: resp = future.result() if send_resp: self._dispatch_callback(msg, cid, mid, cast, cmd_name, resp) def _dispatch_callback(self, msg, cid, mid, cast, cmd_name, resp=None): if resp is None: resp = ok() if not isinstance(resp, (dict, list)): msg = "msg %r tried to send a non-dict: %s" % (msg, str(resp)) logger.error("msg %r tried to send a non-dict: %s", msg, str(resp)) return self.send_error(mid, cid, msg, "server error", cast=cast, errno=errors.BAD_MSG_DATA_ERROR) if isinstance(resp, list): resp = {"results": resp} self.send_ok(mid, cid, msg, resp, cast=cast) if cmd_name.lower() == "quit": if cid is not None: self.stream.flush() self.arbiter.stop() def dispatch(self, job, future=None): cid, msg = job try: json_msg = json.loads(msg) except ValueError: return self.send_error(None, cid, msg, "json invalid", errno=errors.INVALID_JSON) mid = json_msg.get('id') cmd_name = json_msg.get('command') properties = json_msg.get('properties', {}) cast = json_msg.get('msg_type') == "cast" try: cmd = self.commands[cmd_name.lower()] except KeyError: error_ = "unknown command: %r" % cmd_name return self.send_error(mid, cid, msg, error_, cast=cast, errno=errors.UNKNOWN_COMMAND) try: cmd.validate(properties) resp = cmd.execute(self.arbiter, properties) if isinstance(resp, Future): if properties.get('waiting', False): cb = functools.partial(self._dispatch_callback_future, msg, cid, mid, cast, cmd_name, True) resp.add_done_callback(cb) else: cb = functools.partial(self._dispatch_callback_future, msg, cid, mid, cast, cmd_name, False) resp.add_done_callback(cb) self._dispatch_callback(msg, cid, mid, cast, cmd_name, None) else: self._dispatch_callback(msg, cid, mid, cast, cmd_name, resp) except MessageError as e: return self.send_error(mid, cid, msg, str(e), cast=cast, errno=errors.MESSAGE_ERROR) except ConflictError as e: if self._managing_watchers_future is not None: logger.debug("the command conflicts with running " "manage_watchers, re-executing it at " "the end") cb = functools.partial(self.dispatch, job) self.loop.add_future(self._managing_watchers_future, cb) return # conflicts between two commands, sending error... return self.send_error(mid, cid, msg, str(e), cast=cast, errno=errors.COMMAND_ERROR) except OSError as e: return self.send_error(mid, cid, msg, str(e), cast=cast, errno=errors.OS_ERROR) except: exctype, value = sys.exc_info()[:2] tb = traceback.format_exc() reason = "command %r: %s" % (msg, value) logger.debug("error: command %r: %s\n\n%s", msg, value, tb) return self.send_error(mid, cid, msg, reason, tb, cast=cast, errno=errors.COMMAND_ERROR) def send_error(self, mid, cid, msg, reason="unknown", tb=None, cast=False, errno=errors.NOT_SPECIFIED): resp = error(reason=reason, tb=tb, errno=errno) self.send_response(mid, cid, msg, resp, cast=cast) def send_ok(self, mid, cid, msg, props=None, cast=False): resp = ok(props) self.send_response(mid, cid, msg, resp, cast=cast) def send_response(self, mid, cid, msg, resp, cast=False): if cast: return if cid is None: return if isinstance(resp, string_types): raise DeprecationWarning('Takes only a mapping') resp['id'] = mid resp = json.dumps(resp) try: self.stream.send(cid, zmq.SNDMORE) self.stream.send(resp) except (IOError, zmq.ZMQError) as e: logger.debug("Received %r - Could not send back %r - %s", msg, resp, str(e)) circus-0.12.1/circus/exc.py000066400000000000000000000007361256046442300155200ustar00rootroot00000000000000 class AlreadyExist(Exception): """Raised when a watcher exists """ pass class MessageError(Exception): """ error raised when a message is invalid """ pass class CallError(Exception): pass class ArgumentError(Exception): """Exception raised when one argument or the number of arguments is invalid""" pass class ConflictError(Exception): """Exception raised when one exclusive command is already running in background""" pass circus-0.12.1/circus/fixed_threading.py000066400000000000000000000002641256046442300200610ustar00rootroot00000000000000from . import _patch # NOQA from threading import Thread, RLock, Timer # NOQA try: from _thread import get_ident except ImportError: from thread import get_ident # NOQA circus-0.12.1/circus/green/000077500000000000000000000000001256046442300154615ustar00rootroot00000000000000circus-0.12.1/circus/green/__init__.py000066400000000000000000000007021256046442300175710ustar00rootroot00000000000000from circus import ArbiterHandler as _ArbiterHandler class ArbiterHandler(_ArbiterHandler): def __call__(self, watchers, **kwargs): return super(ArbiterHandler, self).__call__(watchers, **kwargs) def _get_arbiter_klass(self, background): if background: raise NotImplementedError else: from circus.green.arbiter import Arbiter # NOQA return Arbiter get_arbiter = ArbiterHandler() circus-0.12.1/circus/green/arbiter.py000066400000000000000000000007361256046442300174710ustar00rootroot00000000000000from circus.arbiter import Arbiter as _Arbiter from circus.green.controller import Controller from zmq.green.eventloop import ioloop from zmq.green import Context class Arbiter(_Arbiter): def _init_context(self, context): self.context = context or Context.instance() self.loop = ioloop.IOLoop.instance() self.ctrl = Controller(self.endpoint, self.multicast_endpoint, self.context, self.loop, self, self.check_delay) circus-0.12.1/circus/green/client.py000066400000000000000000000005251256046442300173130ustar00rootroot00000000000000from circus.client import CircusClient as _CircusClient from zmq.green import Context, Poller, POLLIN class CircusClient(_CircusClient): def _init_context(self, context): self.context = context or Context.instance() def _init_poller(self): self.poller = Poller() self.poller.register(self.socket, POLLIN) circus-0.12.1/circus/green/consumer.py000066400000000000000000000005351256046442300176710ustar00rootroot00000000000000from circus.consumer import CircusConsumer as _CircusConsumer from zmq.green import Context, Poller, POLLIN class CircusConsumer(_CircusConsumer): def _init_context(self, context): self.context = context or Context() def _init_poller(self): self.poller = Poller() self.poller.register(self.pubsub_socket, POLLIN) circus-0.12.1/circus/green/controller.py000066400000000000000000000011741256046442300202210ustar00rootroot00000000000000from circus.controller import Controller as _Controller from circus.green.sighandler import SysHandler from zmq.green.eventloop import ioloop, zmqstream class Controller(_Controller): def _init_syshandler(self): self.sys_hdl = SysHandler(self) def _init_stream(self): self.stream = zmqstream.ZMQStream(self.ctrl_socket, self.loop) self.stream.on_recv(self.handle_message) def start(self): self.initialize() self.caller = ioloop.PeriodicCallback(self.arbiter.manage_watchers, self.check_delay, self.loop) self.caller.start() circus-0.12.1/circus/green/sighandler.py000066400000000000000000000003241256046442300201520ustar00rootroot00000000000000import gevent from circus.sighandler import SysHandler as _SysHandler class SysHandler(_SysHandler): def _register(self): for sig in self.SIGNALS: gevent.signal(sig, self.signal, sig) circus-0.12.1/circus/papa_process_proxy.py000066400000000000000000000105671256046442300206640ustar00rootroot00000000000000from circus.process import Process, debuglog from circus.util import papa from circus import logger import psutil import select import time from circus.py3compat import PY2 __author__ = 'Scott Maxwell' if PY2: class TimeoutError(Exception): """The operation timed out.""" def _bools_to_papa_out(pipe, close): return papa.PIPE if pipe else papa.DEVNULL if close else None class PapaProcessWorker(psutil.Process): # noinspection PyMissingConstructor def __init__(self, proxy, pid): self._init(pid, _ignore_nsp=True) self._proxy = proxy def wait(self, timeout=None): return self._proxy.wait(timeout) class PapaProcessProxy(Process): def __init__(self, *args, **kwargs): self._papa = None self._papa_watcher = None self._papa_name = None super(PapaProcessProxy, self).__init__(*args, **kwargs) def _fix_socket_name(self, s, socket_names): if s: s_lower = s.lower() while '$(circus.sockets.' in s_lower: start = s_lower.index('$(circus.sockets.') end = s_lower.index(')', start) socket_name = s_lower[start + 17:end] if socket_name not in socket_names: logger.warning('Process "{0}" refers to socket "{1}" but ' 'they do not have the same "use_papa" state' .format(self.name, socket_name)) s = ''.join((s[:start], '$(socket.circus.', socket_name, '.fileno)', s[end + 1:])) s_lower = s.lower() return s def spawn(self): # noinspection PyUnusedLocal socket_names = set(socket_name.lower() for socket_name in self.watcher._get_sockets_fds()) self.cmd = self._fix_socket_name(self.cmd, socket_names) self.args = [self._fix_socket_name(arg, socket_names) for arg in self.args] args = self.format_args() stdout = _bools_to_papa_out(self.pipe_stdout, self.close_child_stdout) stderr = _bools_to_papa_out(self.pipe_stderr, self.close_child_stderr) papa_name = 'circus.{0}.{1}'.format(self.name, self.wid).lower() self._papa_name = papa_name self._papa = papa.Papa() try: p = self._papa.make_process(papa_name, executable=self.executable, args=args, env=self.env, working_dir=self.working_dir, uid=self.uid, gid=self.gid, rlimits=self.rlimits, stdout=stdout, stderr=stderr) except papa.Error: p = self._papa.list_processes(papa_name) if p: p = p[papa_name] logger.warning('Process "%s" wid "%d" already exists in papa. ' 'Using the existing process.', self.name, self.wid) else: raise self._worker = PapaProcessWorker(self, p['pid']) self._papa_watcher = self._papa.watch_processes(papa_name) self.started = p['started'] def returncode(self): exit_code = self._papa_watcher.exit_code.get(self._papa_name) if exit_code is None and not self.redirected and\ self._papa_watcher.ready: self._papa_watcher.read() exit_code = self._papa_watcher.exit_code.get(self._papa_name) return exit_code @debuglog def poll(self): return self.returncode() def close_output_channels(self): self._papa_watcher.close() if self.is_alive(): try: self._papa.remove_processes(self._papa_name) except papa.Error: pass def wait(self, timeout=None): until = time.time() + timeout if timeout else None while self.is_alive(): if until: now = time.time() if now >= until: raise TimeoutError() timeout = until - now select.select([self._papa_watcher], [], [], timeout) return self.returncode() @property def output(self): """Return the output watcher""" return self._papa_watcher circus-0.12.1/circus/pidfile.py000066400000000000000000000042161256046442300163520ustar00rootroot00000000000000import errno import os import tempfile class Pidfile(object): """ Manage a PID file. If a specific name is provided it and '"%s.oldpid" % name' will be used. Otherwise we create a temp file using os.mkstemp. """ def __init__(self, fname): self.fname = fname self.pid = None def create(self, pid): oldpid = self.validate() if oldpid: if oldpid == os.getpid(): return raise RuntimeError("Already running on PID %s (or pid file '%s' " "is stale)" % (os.getpid(), self.fname)) self.pid = pid # Write pidfile if self.fname: fdir = os.path.dirname(self.fname) if fdir and not os.path.isdir(fdir): raise RuntimeError("%s doesn't exist. Can't create" "pidfile" % fdir) fd = os.open(self.fname, os.O_CREAT | os.O_WRONLY) else: fd, self.fname = tempfile.mkstemp(dir=fdir) os.write(fd, "{0}\n".format(self.pid).encode('utf-8')) os.close(fd) # set permissions to -rw-r--r-- os.chmod(self.fname, 420) def rename(self, path): self.unlink() self.fname = path self.create(self.pid) def unlink(self): """ delete pidfile""" try: with open(self.fname, "r") as f: pid1 = int(f.read() or 0) if pid1 == self.pid: os.unlink(self.fname) except: pass def validate(self): """ Validate pidfile and make it stale if needed""" if not self.fname: return try: with open(self.fname, "r") as f: wpid = int(f.read() or 0) if wpid <= 0: return try: os.kill(wpid, 0) return wpid except OSError as e: if e.args[0] == errno.ESRCH: return raise except IOError as e: if e.args[0] == errno.ENOENT: return raise circus-0.12.1/circus/plugins/000077500000000000000000000000001256046442300160425ustar00rootroot00000000000000circus-0.12.1/circus/plugins/__init__.py000066400000000000000000000171051256046442300201570ustar00rootroot00000000000000""" Base class to create Circus subscribers plugins. """ import sys import errno import uuid import argparse import zmq import zmq.utils.jsonapi as json from zmq.eventloop import ioloop, zmqstream from circus import logger, __version__ from circus.client import make_message, cast_message from circus.py3compat import b, s from circus.util import (debuglog, to_bool, resolve_name, configure_logger, DEFAULT_ENDPOINT_DEALER, DEFAULT_ENDPOINT_SUB, get_connection) class CircusPlugin(object): """Base class to write plugins. Options: - **context** -- the ZMQ context to use - **endpoint** -- the circusd ZMQ endpoint - **pubsub_endpoint** -- the circusd ZMQ pub/sub endpoint - **check_delay** -- the configured check delay - **config** -- free config mapping """ name = '' def __init__(self, endpoint, pubsub_endpoint, check_delay, ssh_server=None, **config): self.daemon = True self.config = config self.active = to_bool(config.get('active', True)) self.pubsub_endpoint = pubsub_endpoint self.endpoint = endpoint self.check_delay = check_delay self.ssh_server = ssh_server self._id = b(uuid.uuid4().hex) self.running = False self.loop = ioloop.IOLoop() @debuglog def initialize(self): self.context = zmq.Context() self.client = self.context.socket(zmq.DEALER) self.client.setsockopt(zmq.IDENTITY, self._id) get_connection(self.client, self.endpoint, self.ssh_server) self.client.linger = 0 self.sub_socket = self.context.socket(zmq.SUB) self.sub_socket.setsockopt(zmq.SUBSCRIBE, b'watcher.') self.sub_socket.connect(self.pubsub_endpoint) self.substream = zmqstream.ZMQStream(self.sub_socket, self.loop) self.substream.on_recv(self.handle_recv) @debuglog def start(self): if not self.active: raise ValueError('Will not start an inactive plugin') self.handle_init() self.initialize() self.running = True while True: try: self.loop.start() except zmq.ZMQError as e: logger.debug(str(e)) if e.errno == errno.EINTR: continue elif e.errno == zmq.ETERM: break else: logger.debug("got an unexpected error %s (%s)", str(e), e.errno) raise else: break self.substream.close() self.client.close() self.sub_socket.close() self.context.destroy() @debuglog def stop(self): if not self.running: self.loop.close() return try: self.handle_stop() finally: self.loop.stop() self.running = False def call(self, command, **props): """Sends the command to **circusd** Options: - **command** -- the command to call - **props** -- keyword arguments to add to the call Returns the JSON mapping sent back by **circusd** """ msg = make_message(command, **props) self.client.send(json.dumps(msg)) msg = self.client.recv() return json.loads(msg) def cast(self, command, **props): """Fire-and-forget a command to **circusd** Options: - **command** -- the command to call - **props** -- keyword arguments to add to the call """ msg = cast_message(command, **props) self.client.send(json.dumps(msg)) # # methods to override. # def handle_recv(self, data): """Receives every event published by **circusd** Options: - **data** -- a tuple containing the topic and the message. """ raise NotImplementedError() def handle_stop(self): """Called right before the plugin is stopped by Circus. """ pass def handle_init(self): """Called right before a plugin is started - in the thread context. """ pass @staticmethod def split_data(data): topic, msg = data topic_parts = s(topic).split(".") return topic_parts[1], topic_parts[2], msg @staticmethod def load_message(msg): return json.loads(msg) def _cfg2str(cfg): return ':::'.join(['%s:%s' % (key, val) for key, val in cfg.items()]) def _str2cfg(data): cfg = {} if data is None: return cfg for item in data.split(':::'): item = item.split(':', 1) if len(item) != 2: continue key, value = item cfg[key.strip()] = value.strip() return cfg def get_plugin_cmd(config, endpoint, pubsub, check_delay, ssh_server, debug=False, loglevel=None, logoutput=None): fqn = config['use'] # makes sure the name exists resolve_name(fqn) # we're good, serializing the config del config['use'] config = _cfg2str(config) cmd = "%s -c 'from circus import plugins;plugins.main()'" % sys.executable cmd += ' --endpoint %s' % endpoint cmd += ' --pubsub %s' % pubsub if ssh_server is not None: cmd += ' --ssh %s' % ssh_server if len(config) > 0: cmd += ' --config %s' % config if debug: cmd += ' --log-level DEBUG' elif loglevel: cmd += ' --log-level ' + loglevel if logoutput: cmd += ' --log-output ' + logoutput cmd += ' %s' % fqn return cmd def main(): parser = argparse.ArgumentParser(description='Runs a plugin.') parser.add_argument('--endpoint', help='The circusd ZeroMQ socket to connect to', default=DEFAULT_ENDPOINT_DEALER) parser.add_argument('--pubsub', help='The circusd ZeroMQ pub/sub socket to connect to', default=DEFAULT_ENDPOINT_SUB) parser.add_argument('--config', help='The plugin configuration', default=None) parser.add_argument('--version', action='store_true', default=False, help='Displays Circus version and exits.') parser.add_argument('--check-delay', type=float, default=5., help='Checck delay.') parser.add_argument('plugin', help='Fully qualified name of the plugin class.', nargs='?') parser.add_argument('--log-level', dest='loglevel', default='info', help="log level") parser.add_argument('--log-output', dest='logoutput', default='-', help="log output") parser.add_argument('--ssh', default=None, help='SSH Server') args = parser.parse_args() if args.version: print(__version__) sys.exit(0) if args.plugin is None: parser.print_usage() sys.exit(0) factory = resolve_name(args.plugin) # configure the logger configure_logger(logger, args.loglevel, args.logoutput, name=factory.name) # load the plugin and run it. logger.info('Loading the plugin...') logger.info('Endpoint: %r' % args.endpoint) logger.info('Pub/sub: %r' % args.pubsub) plugin = factory(args.endpoint, args.pubsub, args.check_delay, args.ssh, **_str2cfg(args.config)) logger.info('Starting') try: plugin.start() except KeyboardInterrupt: pass finally: logger.info('Stopping') plugin.stop() sys.exit(0) if __name__ == '__main__': main() circus-0.12.1/circus/plugins/_statsd.py000066400000000000000000000001341256046442300200530ustar00rootroot00000000000000# kept for backwards compatibility from circus.plugins.statsd import StatsdEmitter # NOQA circus-0.12.1/circus/plugins/command_reloader.py000066400000000000000000000036451256046442300217170ustar00rootroot00000000000000import os from circus.plugins import CircusPlugin from circus import logger from zmq.eventloop import ioloop class CommandReloader(CircusPlugin): name = 'command_reloader' def __init__(self, *args, **config): super(CommandReloader, self).__init__(*args, **config) self.name = config.get('name') self.loop_rate = int(self.config.get('loop_rate', 1)) self.cmd_files = {} def is_modified(self, watcher, current_mtime, current_path): if watcher not in self.cmd_files: return False if current_mtime != self.cmd_files[watcher]['mtime']: return True if current_path != self.cmd_files[watcher]['path']: return True return False def look_after(self): list_ = self.call('list') watchers = [watcher for watcher in list_['watchers'] if not watcher.startswith('plugin:')] for watcher in list(self.cmd_files.keys()): if watcher not in watchers: del self.cmd_files[watcher] for watcher in watchers: watcher_info = self.call('get', name=watcher, keys=['cmd']) cmd = watcher_info['options']['cmd'] cmd_path = os.path.realpath(cmd) cmd_mtime = os.stat(cmd_path).st_mtime if self.is_modified(watcher, cmd_mtime, cmd_path): logger.info('%s modified. Restarting.', cmd_path) self.call('restart', name=watcher) self.cmd_files[watcher] = { 'path': cmd_path, 'mtime': cmd_mtime, } def handle_init(self): self.period = ioloop.PeriodicCallback(self.look_after, self.loop_rate * 1000, self.loop) self.period.start() def handle_stop(self): self.period.stop() def handle_recv(self, data): pass circus-0.12.1/circus/plugins/flapping.py000066400000000000000000000120561256046442300202200ustar00rootroot00000000000000from circus.fixed_threading import Timer import time from circus import logger from circus.plugins import CircusPlugin from circus.util import to_bool INFINITE_RETRY = -1 class Flapping(CircusPlugin): """ Plugin that controls the flapping and stops the watcher in case it happens too often. Plugin Options -- all of them can be overriden in the watcher options with a *flapping.* prefix: - **attempts** -- number of times a process can restart before we start to detect the flapping (default: 2) - **window** -- the time window in seconds to test for flapping. If the process restarts more than **times** times, we consider it a flapping process. (default: 1) - **retry_in**: time in seconds to wait until we try to start a process that has been flapping. (default: 7) - **max_retry**: the number of times we attempt to start a process, before we abandon and stop the whole watcher. (default: 5) Set to -1 to disable max_retry and retry indefinitely. - **active** -- define if the plugin is active or not (default: True). If the global flag is set to False, the plugin is not started. """ name = 'flapping' def __init__(self, endpoint, pubsub_endpoint, check_delay, ssh_server, **config): super(Flapping, self).__init__(endpoint, pubsub_endpoint, check_delay, ssh_server=ssh_server, **config) self.timelines = {} self.timers = {} self.configs = {} self.tries = {} # default options self.attempts = int(config.get('attempts', 2)) self.window = float(config.get('window', 1)) self.retry_in = float(config.get('retry_in', 7)) self.max_retry = int(config.get('max_retry', 5)) def handle_stop(self): for timer in list(self.timers.values()): timer.cancel() def handle_recv(self, data): watcher_name, action, msg = self.split_data(data) if action == "reap": timeline = self.timelines.get(watcher_name, []) timeline.append(time.time()) self.timelines[watcher_name] = timeline self.check(watcher_name) elif action == "updated": self.update_conf(watcher_name) def update_conf(self, watcher_name): msg = self.call("options", name=watcher_name) conf = self.configs.get(watcher_name, {}) for key, value in msg.get('options', {}).items(): key = key.split('.') if key[0] != self.name: continue key = '.'.join(key[1:]) if key in ('attempts', 'max_retry'): value = int(value) elif key in ('window', 'retry_in'): value = float(value) conf[key] = value self.configs[watcher_name] = conf return conf def reset(self, watcher_name): self.timelines[watcher_name] = [] self.tries[watcher_name] = 0 if watcher_name is self.timers: timer = self.timers.pop(watcher_name) timer.cancel() def _get_conf(self, conf, name): return conf.get(name, getattr(self, name)) def check(self, watcher_name): timeline = self.timelines[watcher_name] if watcher_name in self.configs: conf = self.configs[watcher_name] else: conf = self.update_conf(watcher_name) # if the watcher is not activated, we skip it if not to_bool(self._get_conf(conf, 'active')): # nothing to do here return tries = self.tries.get(watcher_name, 0) if len(timeline) == self._get_conf(conf, 'attempts'): duration = timeline[-1] - timeline[0] - self.check_delay if duration <= self._get_conf(conf, 'window'): max_retry = self._get_conf(conf, 'max_retry') if tries < max_retry or max_retry == INFINITE_RETRY: next_tries = tries + 1 logger.info("%s: flapping detected: retry in %2ds " "(attempt number %s)", watcher_name, self._get_conf(conf, 'retry_in'), next_tries) self.cast("stop", name=watcher_name) self.timelines[watcher_name] = [] self.tries[watcher_name] = next_tries def _start(): self.cast("start", name=watcher_name) timer = Timer(self._get_conf(conf, 'retry_in'), _start) timer.start() self.timers[watcher_name] = timer else: logger.info( "%s: flapping detected: reached max retry limit", watcher_name) self.timelines[watcher_name] = [] self.tries[watcher_name] = 0 self.cast("stop", name=watcher_name) else: self.timelines[watcher_name] = [] self.tries[watcher_name] = 0 circus-0.12.1/circus/plugins/http_observer.py000066400000000000000000000025251256046442300213060ustar00rootroot00000000000000 from circus.plugins.statsd import BaseObserver try: from tornado.httpclient import AsyncHTTPClient except ImportError: raise ImportError("This plugin requires tornado-framework to run.") class HttpObserver(BaseObserver): name = 'http_observer' default_app_name = "http_observer" def __init__(self, *args, **config): super(HttpObserver, self).__init__(*args, **config) self.http_client = AsyncHTTPClient(io_loop=self.loop) self.check_url = config.get("check_url", "http://localhost/") self.timeout = float(config.get("timeout", 10)) self.restart_on_error = config.get("restart_on_error", None) def look_after(self): def handle_response(response, *args, **kwargs): if response.error: self.statsd.increment("http_stats.error") self.statsd.increment("http_stats.error.%s" % response.code) if self.restart_on_error: self.cast("restart", name=self.restart_on_error) self.statsd.increment("http_stats.restart_on_error") return self.statsd.timed("http_stats.request_time", int(response.request_time * 1000)) self.http_client.fetch(self.check_url, handle_response, request_timeout=self.timeout) circus-0.12.1/circus/plugins/redis_observer.py000066400000000000000000000032341256046442300214330ustar00rootroot00000000000000 from circus.plugins.statsd import BaseObserver try: import redis except ImportError: raise ImportError("This plugin requires the redis-lib to run.") class RedisObserver(BaseObserver): name = 'redis_observer' default_app_name = "redis_observer" OBSERVE = ['pubsub_channels', 'connected_slaves', 'lru_clock', 'connected_clients', 'keyspace_misses', 'used_memory', 'used_memory_peak', 'total_commands_processed', 'used_memory_rss', 'total_connections_received', 'pubsub_patterns', 'used_cpu_sys', 'used_cpu_sys_children', 'blocked_clients', 'used_cpu_user', 'client_biggest_input_buf', 'mem_fragmentation_ratio', 'expired_keys', 'evicted_keys', 'client_longest_output_list', 'uptime_in_seconds', 'keyspace_hits'] def __init__(self, *args, **config): super(RedisObserver, self).__init__(*args, **config) self.redis = redis.from_url(config.get("redis_url", "redis://localhost:6379/0"), float(config.get("timeout", 5))) self.restart_on_timeout = config.get("restart_on_timeout", None) def look_after(self): try: info = self.redis.info() except redis.ConnectionError: self.statsd.increment("redis_stats.error") if self.restart_on_timeout: self.cast("restart", name=self.restart_on_timeout) self.statsd.increment("redis_stats.restart_on_error") return for key in self.OBSERVE: self.statsd.gauge("redis_stats.%s" % key, info[key]) circus-0.12.1/circus/plugins/resource_watcher.py000066400000000000000000000176771256046442300220020ustar00rootroot00000000000000import signal import warnings from circus.plugins.statsd import BaseObserver from circus.util import to_bool from circus.util import human2bytes class ResourceWatcher(BaseObserver): def __init__(self, *args, **config): super(ResourceWatcher, self).__init__(*args, **config) self.watcher = config.get("watcher", None) self.service = config.get("service", None) if self.service is not None: warnings.warn("ResourceWatcher.service is deprecated " "please use ResourceWatcher.watcher instead.", category=DeprecationWarning) if self.watcher is None: self.watcher = self.service if self.watcher is None: self.statsd.stop() self.loop.close() raise NotImplementedError('watcher is mandatory for now.') self.max_cpu = float(config.get("max_cpu", 90)) # in % self.max_mem = config.get("max_mem") if self.max_mem is None: self.max_mem = 90. self._max_percent = True else: try: self.max_mem = float(self.max_mem) # float -> % self._max_percent = True except ValueError: self.max_mem = human2bytes(self.max_mem) # int -> absolute self._max_percent = False self.min_cpu = config.get("min_cpu") if self.min_cpu is not None: self.min_cpu = float(self.min_cpu) # in % self.min_mem = config.get("min_mem") if self.min_mem is not None: try: self.min_mem = float(self.min_mem) # float -> % self._min_percent = True except ValueError: self.min_mem = human2bytes(self.min_mem) # int -> absolute self._min_percent = True self.health_threshold = float(config.get("health_threshold", 75)) # in % self.max_count = int(config.get("max_count", 3)) self.process_children = to_bool(config.get("process_children", '0')) self.child_signal = int(config.get("child_signal", signal.SIGTERM)) self._count_over_cpu = {} self._count_over_mem = {} self._count_under_cpu = {} self._count_under_mem = {} self._count_health = {} def look_after(self): info = self.call("stats", name=self.watcher) if info["status"] == "error": self.statsd.increment("_resource_watcher.%s.error" % self.watcher) return stats = info['info'] self._process_index('parent', self._collect_data(stats)) if not self.process_children: return for sub_info in stats.values(): if isinstance(sub_info, dict): for child_info in sub_info['children']: data = self._collect_data({child_info['pid']: child_info}) self._process_index(child_info['pid'], data) def _collect_data(self, stats): data = {} cpus = [] mems = [] mems_abs = [] for sub_info in stats.values(): if isinstance(sub_info, dict): cpus.append(100 if sub_info['cpu'] == 'N/A' else float(sub_info['cpu'])) mems.append(100 if sub_info['mem'] == 'N/A' else float(sub_info['mem'])) mems_abs.append(0 if sub_info['mem_info1'] == 'N/A' else human2bytes(sub_info['mem_info1'])) if cpus: data['max_cpu'] = max(cpus) data['max_mem'] = max(mems) data['max_mem_abs'] = max(mems_abs) data['min_cpu'] = min(cpus) data['min_mem'] = min(mems) data['min_mem_abs'] = min(mems_abs) else: # we dont' have any process running. max = 0 then data['max_cpu'] = 0 data['max_mem'] = 0 data['min_cpu'] = 0 data['min_mem'] = 0 data['max_mem_abs'] = 0 data['min_mem_abs'] = 0 return data def _process_index(self, index, stats): if (index not in self._count_over_cpu or index not in self._count_over_mem or index not in self._count_under_cpu or index not in self._count_under_mem or index not in self._count_health): self._reset_index(index) if self.max_cpu and stats['max_cpu'] > self.max_cpu: self.statsd.increment("_resource_watcher.%s.over_cpu" % self.watcher) self._count_over_cpu[index] += 1 else: self._count_over_cpu[index] = 0 if self.min_cpu is not None and stats['min_cpu'] <= self.min_cpu: self.statsd.increment("_resource_watcher.%s.under_cpu" % self.watcher) self._count_under_cpu[index] += 1 else: self._count_under_cpu[index] = 0 if self.max_mem is not None: over_percent = (self._max_percent and stats['max_mem'] > self.max_mem) over_value = (not self._max_percent and stats['max_mem_abs'] > self.max_mem) if over_percent or over_value: self.statsd.increment("_resource_watcher.%s.over_memory" % self.watcher) self._count_over_mem[index] += 1 else: self._count_over_mem[index] = 0 else: self._count_over_mem[index] = 0 if self.min_mem is not None: under_percent = (self._min_percent and stats['min_mem'] < self.min_mem) under_value = (not self._min_percent and stats['min_mem_abs'] < self.min_mem) if under_percent or under_value: self.statsd.increment("_resource_watcher.%s.under_memory" % self.watcher) self._count_under_mem[index] += 1 else: self._count_under_mem[index] = 0 else: self._count_under_mem[index] = 0 max_cpu = stats['max_cpu'] max_mem = stats['max_mem'] if (self.health_threshold and (max_cpu + max_mem) / 2.0 > self.health_threshold): self.statsd.increment("_resource_watcher.%s.over_health" % self.watcher) self._count_health[index] += 1 else: self._count_health[index] = 0 if max([self._count_over_cpu[index], self._count_under_cpu[index], self._count_over_mem[index], self._count_under_mem[index], self._count_health[index]]) > self.max_count: self.statsd.increment("_resource_watcher.%s.restarting" % self.watcher) # todo: restart only process instead of the whole watcher if index == 'parent': self.cast("restart", name=self.watcher) self._reset_index(index) else: self.cast( "signal", name=self.watcher, signum=self.child_signal, child_pid=index ) self._remove_index(index) self._reset_index(index) def _reset_index(self, index): self._count_over_cpu[index] = 0 self._count_over_mem[index] = 0 self._count_under_cpu[index] = 0 self._count_under_mem[index] = 0 self._count_health[index] = 0 def _remove_index(self, index): del self._count_over_cpu[index] del self._count_over_mem[index] del self._count_under_cpu[index] del self._count_under_mem[index] del self._count_health[index] def stop(self): self.statsd.stop() super(ResourceWatcher, self).stop() circus-0.12.1/circus/plugins/statsd.py000066400000000000000000000110441256046442300177160ustar00rootroot00000000000000import socket from zmq.eventloop import ioloop from circus.plugins import CircusPlugin from circus.util import human2bytes class StatsdClient(object): def __init__(self, host=None, port=None, prefix=None, sample_rate=1): self.host = host self.port = port self.prefix = prefix self.sample_rate = sample_rate self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) def send(self, bucket, value, sample_rate=None): sample_rate = sample_rate or self.sample_rate if sample_rate != 1: value += "|@%s" % sample_rate if self.prefix: bucket = "%s.%s" % (self.prefix, bucket) self.socket.sendto("%s:%s" % (bucket, value), (self.host, self.port)) def decrement(self, bucket, delta=1): if delta > 0: delta = - delta self.increment(bucket, delta) def increment(self, bucket, delta=1): self.send(bucket, "%d|c" % delta) def gauge(self, bucket, value): self.send(bucket, "%s|g" % value) def timed(self, bucket, value): self.send(bucket, "%s|ms" % value) def stop(self): self.socket.close() class StatsdEmitter(CircusPlugin): """Plugin that sends stuff to statsd """ name = 'statsd' default_app_name = "app" def __init__(self, endpoint, pubsub_endpoint, check_delay, ssh_server, **config): super(StatsdEmitter, self).__init__(endpoint, pubsub_endpoint, check_delay, ssh_server=ssh_server) self.app = config.get('application_name', self.default_app_name) self.prefix = 'circus.%s.watcher' % self.app # initialize statsd self.statsd = StatsdClient(host=config.get('host', 'localhost'), port=int(config.get('port', '8125')), prefix=self.prefix, sample_rate=float( config.get('sample_rate', '1.0'))) def handle_recv(self, data): watcher_name, action, msg = self.split_data(data) self.statsd.increment('%s.%s' % (watcher_name, action)) def stop(self): self.statsd.stop() super(StatsdEmitter, self).stop() class BaseObserver(StatsdEmitter): def __init__(self, *args, **config): super(BaseObserver, self).__init__(*args, **config) self.loop_rate = float(config.get("loop_rate", 60)) # in seconds def handle_init(self): self.period = ioloop.PeriodicCallback(self.look_after, self.loop_rate * 1000, self.loop) self.period.start() def handle_stop(self): self.period.stop() self.statsd.stop() def handle_recv(self, data): pass def look_after(self): raise NotImplementedError() class FullStats(BaseObserver): name = 'full_stats' def look_after(self): info = self.call("stats") if info["status"] == "error": self.statsd.increment("_stats.error") return for name, stats in info['infos'].items(): if name.startswith("plugin:"): # ignore plugins continue cpus = [] mems = [] mem_infos = [] for sub_name, sub_info in stats.items(): if isinstance(sub_info, dict): cpus.append(sub_info['cpu']) mems.append(sub_info['mem']) mem_infos.append(human2bytes(sub_info['mem_info1'])) elif sub_name == "spawn_count": # spawn_count info is in the same level as processes # dict infos, so if spawn_count is given, take it and # continue self.statsd.gauge("_stats.%s.spawn_count" % name, sub_info) self.statsd.gauge("_stats.%s.watchers_num" % name, len(cpus)) if not cpus: # if there are only dead processes, we have an empty list # and we can't measure it continue self.statsd.gauge("_stats.%s.cpu_max" % name, max(cpus)) self.statsd.gauge("_stats.%s.cpu_sum" % name, sum(cpus)) self.statsd.gauge("_stats.%s.mem_pct_max" % name, max(mems)) self.statsd.gauge("_stats.%s.mem_pct_sum" % name, sum(mems)) self.statsd.gauge("_stats.%s.mem_max" % name, max(mem_infos)) self.statsd.gauge("_stats.%s.mem_sum" % name, sum(mem_infos)) circus-0.12.1/circus/plugins/watchdog.py000066400000000000000000000216331256046442300202210ustar00rootroot00000000000000import re import socket import time import signal from zmq.eventloop import ioloop from circus.plugins import CircusPlugin from circus import logger class WatchDog(CircusPlugin): """Plugin that bind an udp socket and wait for watchdog messages. For "watchdoged" processes, the watchdog will kill them if they don't send heartbeat in a certain period of time materialized by loop_rate * max_count. (circus will automatically restart the missing processes in the watcher) Each monitored process should send udp message at least at the loop_rate. The udp message format is a line of text, decoded using **msg_regex** parameter. The heartbeat message MUST at least contain the pid of the process sending the message. The list of monitored watchers are determined by the parameter **watchers_regex** in the configuration. At startup, the plugin does not know all the circus watchers and pids, so it's needed to discover all watchers and pids. After the discover, the monitoring list is updated by messages from circusd handled in self.handle_recv Plugin Options -- - **loop_rate** -- watchdog loop rate in seconds. At each loop, WatchDog will looks for "dead" processes. - **watchers_regex** -- regex for matching watcher names that should be monitored by the watchdog (default: ".*" all watchers are monitored) - **msg_regex** -- regex for decoding the received heartbeat message in udp (default: "^(?P.*);(?P.*)$") the default format is a simple text message: "pid;timestamp" - **max_count** -- max number of passed loop without receiving any heartbeat before restarting process (default: 3) - **ip** -- ip the watchdog will bind on (default: 127.0.0.1) - **port** -- port the watchdog will bind on (default: 1664) """ name = 'watchdog' def __init__(self, endpoint, pubsub_endpoint, check_delay, ssh_server, **config): super(WatchDog, self).__init__(endpoint, pubsub_endpoint, check_delay, ssh_server=ssh_server) self.loop_rate = float(config.get("loop_rate", 60)) # in seconds self.watchers_regex = config.get("watchers_regex", ".*") self.msg_regex = config.get("msg_regex", "^(?P.*);(?P.*)$") self.max_count = config.get("max_count", 3) self.watchdog_ip = config.get("ip", "127.0.0.1") self.watchdog_port = config.get("port", 1664) self.pid_status = dict() self.period = None self.starting = True def handle_init(self): """Initialization of plugin - set the periodic call back for the process monitoring (at loop_rate) - create the listening UDP socket """ self.period = ioloop.PeriodicCallback(self.look_after, self.loop_rate * 1000, self.loop) self.period.start() self._bind_socket() def handle_stop(self): if self.period is not None: self.period.stop() self.sock.close() self.sock = None def handle_recv(self, data): """Handle received message from circusd We need to handle two messages: - spawn: add a new monitored child pid - reap: remove a killed child pid from monitoring """ watcher_name, action, msg = self.split_data(data) logger.debug("received data from circusd: watcher.%s.%s, %s", watcher_name, action, msg) # check if monitored watchers: if self._match_watcher_name(watcher_name): try: message = self.load_message(msg) except ValueError: logger.error("Error while decoding json for message: %s", msg) else: if "process_pid" not in message: logger.warning('no process_pid in message') return pid = str(message.get("process_pid")) if action == "spawn": self.pid_status[pid] = dict(watcher=watcher_name, last_activity=time.time()) logger.info("added new monitored pid for %s:%s", watcher_name, pid) # very questionable fix for Py3 here! # had to add check for pid in self.pid_status elif action == "reap" and pid in self.pid_status: old_pid = self.pid_status.pop(pid) logger.info("removed monitored pid for %s:%s", old_pid['watcher'], pid) def _discover_monitored_pids(self): """Try to discover all the monitored pids. This should be done only at startup time, because if new watchers or pids are created in running time, we should receive the message from circusd which is handled by self.handle_recv """ self.pid_status = dict() all_watchers = self.call("list") for watcher_name in all_watchers['watchers']: if self._match_watcher_name(watcher_name): processes = self.call("list", name=watcher_name) if 'pids' in processes: for pid in processes['pids']: pid = str(pid) self.pid_status[pid] = dict(watcher=watcher_name, last_activity=time.time()) logger.info("discovered: %s, pid:%s", watcher_name, pid) def _bind_socket(self): """bind the listening socket for watchdog udp and start an event handler for handling udp received messages. """ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: self.sock.bind((self.watchdog_ip, self.watchdog_port)) except socket.error as socket_error: logger.error( "Problem while binding watchdog socket on %s:%s (err %s", self.watchdog_ip, self.watchdog_port, str(socket_error)) self.sock = None else: self.sock.settimeout(1) self.loop.add_handler(self.sock.fileno(), self.receive_udp_socket, ioloop.IOLoop.READ) logger.info("Watchdog listening UDP on %s:%s", self.watchdog_ip, self.watchdog_port) def _match_watcher_name(self, name): """Match the given watcher name with the watcher_regex given in config :return: re.match object or None """ return re.match(self.watchers_regex, name) def _decode_received_udp_message(self, data): """decode the received message according to the msg_regex :return: decoded message :rtype: dict or None """ result = re.match(self.msg_regex, data) if result is not None: return result.groupdict() def receive_udp_socket(self, fd, events): """Check the socket for received UDP message. This method is periodically called by the ioloop. If messages are received and parsed, update the status of the corresponing pid. """ data, _ = self.sock.recvfrom(1024) heartbeat = self._decode_received_udp_message(data) if "pid" in heartbeat: if heartbeat['pid'] in self.pid_status: # TODO: check and compare received time # with our own time.time() self.pid_status[heartbeat["pid"]][ 'last_activity'] = time.time() else: logger.warning("received watchdog for a" "non monitored process:%s", heartbeat) logger.debug("watchdog message: %s", heartbeat) def look_after(self): """Checks for the watchdoged watchers and restart a process if no received watchdog after the loop_rate * max_count period. """ # if first check, do a full discovery first. if self.starting: self._discover_monitored_pids() self.starting = False max_timeout = self.loop_rate * self.max_count too_old_time = time.time() - max_timeout for pid, detail in self.pid_status.items(): if detail['last_activity'] < too_old_time: logger.info("watcher:%s, pid:%s is not responding. Kill it !", detail['watcher'], pid) self.cast("signal", name=detail['watcher'], pid=int(pid), signum=signal.SIGKILL) circus-0.12.1/circus/process.py000066400000000000000000000465731256046442300164300ustar00rootroot00000000000000try: import ctypes except MemoryError: # selinux execmem denial # https://bugzilla.redhat.com/show_bug.cgi?id=488396 ctypes = None # NOQA except ImportError: # Python on Solaris compiled with Sun Studio doesn't have ctypes ctypes = None # NOQA import sys import errno import os from subprocess import PIPE import time import shlex import warnings try: import resource except ImportError: resource = None # NOQA from psutil import (Popen, STATUS_ZOMBIE, STATUS_DEAD, NoSuchProcess, AccessDenied) from circus.py3compat import bytestring, string_types, quote from circus.sockets import CircusSocket from circus.util import (get_info, to_uid, to_gid, debuglog, get_working_dir, ObjectDict, replace_gnu_args, get_default_gid, get_username_from_uid, IS_WINDOWS) from circus import logger _INFOLINE = ("%(pid)s %(cmdline)s %(username)s %(nice)s %(mem_info1)s " "%(mem_info2)s %(cpu)s %(mem)s %(ctime)s") RUNNING = 0 DEAD_OR_ZOMBIE = 1 UNEXISTING = 2 OTHER = 3 # psutil < 2.x compat def get_children(proc, recursive=False): try: return proc.children(recursive) except AttributeError: return proc.get_children(recursive) def get_memory_info(proc): try: return proc.memory_info() except AttributeError: return proc.get_memory_info() def get_cpu_percent(proc, **kw): try: return proc.cpu_percent(**kw) except AttributeError: return proc.get_cpu_percent(**kw) def get_memory_percent(proc): try: return proc.memory_percent() except AttributeError: return proc.get_memory_percent() def get_cpu_times(proc): try: return proc.cpu_times() except AttributeError: return proc.get_cpu_times() def get_nice(proc): try: return proc.nice() except (AttributeError, TypeError): return proc.get_nice() def get_cmdline(proc): try: return proc.cmdline() except TypeError: return proc.cmdline def get_create_time(proc): try: return proc.create_time() except TypeError: return proc.create_time def get_username(proc): try: return proc.username() except TypeError: return proc.username def get_status(proc): try: return proc.status() except TypeError: return proc.status class Process(object): """Wraps a process. Options: - **wid**: the process unique identifier. This value will be used to replace the *$WID* string in the command line if present. - **cmd**: the command to run. May contain any of the variables available that are being passed to this class. They will be replaced using the python format syntax. - **args**: the arguments for the command to run. Can be a list or a string. If **args** is a string, it's splitted using :func:`shlex.split`. Defaults to None. - **executable**: When executable is given, the first item in the args sequence obtained from **cmd** is still treated by most programs as the command name, which can then be different from the actual executable name. It becomes the display name for the executing program in utilities such as **ps**. - **working_dir**: the working directory to run the command in. If not provided, will default to the current working directory. - **shell**: if *True*, will run the command in the shell environment. *False* by default. **warning: this is a security hazard**. - **uid**: if given, is the user id or name the command should run with. The current uid is the default. - **gid**: if given, is the group id or name the command should run with. The current gid is the default. - **env**: a mapping containing the environment variables the command will run with. Optional. - **rlimits**: a mapping containing rlimit names and values that will be set before the command runs. - **use_fds**: if True, will not close the fds in the subprocess. Must be be set to True on Windows if stdout or stderr are redirected. default: False. - **pipe_stdout**: if True, will open a PIPE on stdout. default: True. - **pipe_stderr**: if True, will open a PIPE on stderr. default: True. - **close_child_stdout**: If True, redirects the child process' stdout to /dev/null after the fork. default: False. - **close_child_stderr**: If True, redirects the child process' stdout to /dev/null after the fork. default: False. """ def __init__(self, name, wid, cmd, args=None, working_dir=None, shell=False, uid=None, gid=None, env=None, rlimits=None, executable=None, use_fds=False, watcher=None, spawn=True, pipe_stdout=True, pipe_stderr=True, close_child_stdout=False, close_child_stderr=False): self.name = name self.wid = wid self.cmd = cmd self.args = args self.working_dir = working_dir or get_working_dir() self.shell = shell if uid: self.uid = to_uid(uid) self.username = get_username_from_uid(self.uid) else: self.username = None self.uid = None self.gid = to_gid(gid) if gid else None self.env = env or {} self.rlimits = rlimits or {} self.executable = executable self.use_fds = use_fds self.watcher = watcher self.pipe_stdout = pipe_stdout self.pipe_stderr = pipe_stderr self.close_child_stdout = close_child_stdout self.close_child_stderr = close_child_stderr self.stopping = False # sockets created before fork, should be let go after. self._sockets = [] self._worker = None self.redirected = False self.started = 0 if self.uid is not None and self.gid is None: self.gid = get_default_gid(self.uid) if IS_WINDOWS: if not self.use_fds and (self.pipe_stderr or self.pipe_stdout): raise ValueError("On Windows, you can't close the fds if " "you are redirecting stdout or stderr") if spawn: self.spawn() def _null_streams(self, streams): devnull = os.open(os.devnull, os.O_RDWR) try: for stream in streams: if not hasattr(stream, 'fileno'): # we're probably dealing with a file-like continue try: stream.flush() os.dup2(devnull, stream.fileno()) except IOError: # some streams, like stdin - might be already closed. pass finally: os.close(devnull) def _get_sockets_fds(self): """Returns sockets dict. If this worker's cmd indicates use of a SO_REUSEPORT socket, a new socket is created and bound. This new socket's FD replaces original socket's FD in returned dict. This method populates `self._sockets` list. This list should be let go after `fork()`. """ sockets_fds = None if self.watcher is not None and self.watcher.sockets is not None: sockets_fds = self.watcher._get_sockets_fds() reuseport_sockets = tuple((sn, s) for (sn, s) in self.watcher.sockets.items() if s.so_reuseport) for sn, s in reuseport_sockets: # watcher.cmd uses this reuseport socket if 'circus.sockets.%s' % sn in self.watcher.cmd: sock = CircusSocket.load_from_config(s._cfg) sock.bind_and_listen() # replace original socket's fd sockets_fds[sn] = sock.fileno() # keep new socket until fork returns self._sockets.append(sock) return sockets_fds def spawn(self): self.started = time.time() sockets_fds = self._get_sockets_fds() args = self.format_args(sockets_fds=sockets_fds) def preexec(): streams = [sys.stdin] if self.close_child_stdout: streams.append(sys.stdout) if self.close_child_stderr: streams.append(sys.stderr) self._null_streams(streams) os.setsid() if resource: for limit, value in self.rlimits.items(): res = getattr( resource, 'RLIMIT_%s' % limit.upper(), None ) if res is None: raise ValueError('unknown rlimit "%s"' % limit) # TODO(petef): support hard/soft limits # for the NOFILE limit, if we fail to set an unlimited # value then check the existing hard limit because we # probably can't bypass it due to a kernel limit - so just # assume that the caller means they want to use the kernel # limit when they pass the unlimited value. This is better # than failing to start the process and forcing the caller # to always be aware of what the kernel configuration is. # If they do pass in a real limit value, then we'll just # raise the failure as they should know that their # expectations couldn't be met. # TODO - we can't log here as this occurs in the child # process after the fork but it would be very good to # notify the admin of the situation somehow. retry = False try: resource.setrlimit(res, (value, value)) except ValueError: if res == resource.RLIMIT_NOFILE and \ value == resource.RLIM_INFINITY: _soft, value = resource.getrlimit(res) retry = True else: raise if retry: resource.setrlimit(res, (value, value)) if self.gid: try: os.setgid(self.gid) except OverflowError: if not ctypes: raise # versions of python < 2.6.2 don't manage unsigned int for # groups like on osx or fedora os.setgid(-ctypes.c_int(-self.gid).value) if self.username is not None: try: os.initgroups(self.username, self.gid) except (OSError, AttributeError): # not support on Mac or 2.6 pass if self.uid: os.setuid(self.uid) if IS_WINDOWS: # On Windows we can't use a pre-exec function preexec_fn = None else: preexec_fn = preexec extra = {} if self.pipe_stdout: extra['stdout'] = PIPE if self.pipe_stderr: extra['stderr'] = PIPE self._worker = Popen(args, cwd=self.working_dir, shell=self.shell, preexec_fn=preexec_fn, env=self.env, close_fds=not self.use_fds, executable=self.executable, **extra) # let go of sockets created only for self._worker to inherit self._sockets = [] def format_args(self, sockets_fds=None): """ It's possible to use environment variables and some other variables that are available in this context, when spawning the processes. """ logger.debug('cmd: ' + bytestring(self.cmd)) logger.debug('args: ' + str(self.args)) current_env = ObjectDict(self.env.copy()) format_kwargs = { 'wid': self.wid, 'shell': self.shell, 'args': self.args, 'env': current_env, 'working_dir': self.working_dir, 'uid': self.uid, 'gid': self.gid, 'rlimits': self.rlimits, 'executable': self.executable, 'use_fds': self.use_fds} if sockets_fds is not None: format_kwargs['sockets'] = sockets_fds if self.watcher is not None: for option in self.watcher.optnames: if option not in format_kwargs\ and hasattr(self.watcher, option): format_kwargs[option] = getattr(self.watcher, option) cmd = replace_gnu_args(self.cmd, **format_kwargs) if '$WID' in cmd or (self.args and '$WID' in self.args): msg = "Using $WID in the command is deprecated. You should use "\ "the python string format instead. In your case, this "\ "means replacing the $WID in your command by $(WID)." warnings.warn(msg, DeprecationWarning) self.cmd = cmd.replace('$WID', str(self.wid)) if self.args is not None: if isinstance(self.args, string_types): args = shlex.split(bytestring(replace_gnu_args( self.args, **format_kwargs))) else: args = [bytestring(replace_gnu_args(arg, **format_kwargs)) for arg in self.args] args = shlex.split(bytestring(cmd), posix=not IS_WINDOWS) + args else: args = shlex.split(bytestring(cmd), posix=not IS_WINDOWS) if self.shell: # subprocess.Popen(shell=True) implies that 1st arg is the # requested command, remaining args are applied to sh. args = [' '.join(quote(arg) for arg in args)] shell_args = format_kwargs.get('shell_args', None) if shell_args and IS_WINDOWS: logger.warn("shell_args won't apply for " "windows platforms: %s", shell_args) elif isinstance(shell_args, string_types): args += shlex.split(bytestring(replace_gnu_args( shell_args, **format_kwargs))) elif shell_args: args += [bytestring(replace_gnu_args(arg, **format_kwargs)) for arg in shell_args] elif format_kwargs.get('shell_args', False): logger.warn("shell_args is defined but won't be used " "in this context: %s", format_kwargs['shell_args']) logger.debug("process args: %s", args) return args def returncode(self): return self._worker.returncode @debuglog def poll(self): return self._worker.poll() @debuglog def is_alive(self): return self.poll() is None @debuglog def send_signal(self, sig): """Sends a signal **sig** to the process.""" logger.debug("sending signal %s to %s" % (sig, self.pid)) return self._worker.send_signal(sig) @debuglog def stop(self): """Stop the process and close stdout/stderr If the corresponding process is still here (normally it's already killed by the watcher), a SIGTERM is sent, then a SIGKILL after 1 second. The shutdown process (SIGTERM then SIGKILL) is normally taken by the watcher. So if the process is still there here, it's a kind of bad behavior because the graceful timeout won't be respected here. """ try: try: if self.is_alive(): try: return self._worker.terminate() except AccessDenied: # It can happen on Windows if the process # dies after poll returns (unlikely) pass finally: self.close_output_channels() except NoSuchProcess: pass def close_output_channels(self): if self._worker.stderr is not None: self._worker.stderr.close() if self._worker.stdout is not None: self._worker.stdout.close() def wait(self, timeout=None): """ Wait for the process to terminate, in the fashion of waitpid. Accepts a timeout in seconds. """ self._worker.wait(timeout) def age(self): """Return the age of the process in seconds.""" return time.time() - self.started def info(self): """Return process info. The info returned is a mapping with these keys: - **mem_info1**: Resident Set Size Memory in bytes (RSS) - **mem_info2**: Virtual Memory Size in bytes (VMS). - **cpu**: % of cpu usage. - **mem**: % of memory usage. - **ctime**: process CPU (user + system) time in seconds. - **pid**: process id. - **username**: user name that owns the process. - **nice**: process niceness (between -20 and 20) - **cmdline**: the command line the process was run with. """ try: info = get_info(self._worker) except NoSuchProcess: return "No such process (stopped?)" info["age"] = self.age() info["started"] = self.started info["children"] = [] info['wid'] = self.wid for child in get_children(self._worker): info["children"].append(get_info(child)) return info def children(self): """Return a list of children pids.""" return [child.pid for child in get_children(self._worker)] def is_child(self, pid): """Return True is the given *pid* is a child of that process.""" pids = [child.pid for child in get_children(self._worker)] if pid in pids: return True return False @debuglog def send_signal_child(self, pid, signum): """Send signal *signum* to child *pid*.""" children = dict((child.pid, child) for child in get_children(self._worker)) try: children[pid].send_signal(signum) except KeyError: raise NoSuchProcess(pid) @debuglog def send_signal_children(self, signum, recursive=False): """Send signal *signum* to all children.""" for child in get_children(self._worker, recursive): try: child.send_signal(signum) except OSError as e: if e.errno != errno.ESRCH: raise @property def status(self): """Return the process status as a constant - RUNNING - DEAD_OR_ZOMBIE - UNEXISTING - OTHER """ try: if get_status(self._worker) in (STATUS_ZOMBIE, STATUS_DEAD): return DEAD_OR_ZOMBIE except NoSuchProcess: return UNEXISTING if self._worker.is_running(): return RUNNING return OTHER @property def pid(self): """Return the *pid*""" return self._worker.pid @property def stdout(self): """Return the *stdout* stream""" return self._worker.stdout @property def stderr(self): """Return the *stdout* stream""" return self._worker.stderr def __eq__(self, other): return self is other def __lt__(self, other): return self.started < other.started def __gt__(self, other): return self.started > other.started circus-0.12.1/circus/py3compat.py000066400000000000000000000061621256046442300166570ustar00rootroot00000000000000import sys PY2 = sys.version_info[0] < 3 if PY2: string_types = basestring # NOQA integer_types = (int, long) # NOQA text_type = unicode # NOQA long = long # NOQA bytes = str def bytestring(s): # NOQA if isinstance(s, unicode): # NOQA return s.encode('utf-8') return s def cast_bytes(s, encoding='utf8'): """cast unicode or bytes to bytes""" if isinstance(s, unicode): return s.encode(encoding) return str(s) def cast_unicode(s, encoding='utf8', errors='replace'): """cast bytes or unicode to unicode. errors options are strict, ignore or replace""" if isinstance(s, unicode): return s return str(s).decode(encoding) def cast_string(s, errors='replace'): return s if isinstance(s, basestring) else str(s) # NOQA try: import cStringIO StringIO = cStringIO.StringIO # NOQA except ImportError: import StringIO StringIO = StringIO.StringIO # NOQA BytesIO = StringIO eval(compile('def raise_with_tb(E): raise E, None, sys.exc_info()[2]', 'py3compat.py', 'exec')) def is_callable(c): # NOQA return callable(c) def get_next(c): # NOQA return c.next # It's possible to have sizeof(long) != sizeof(Py_ssize_t). class X(object): def __len__(self): return 1 << 31 try: len(X()) except OverflowError: # 32-bit MAXSIZE = int((1 << 31) - 1) # NOQA else: # 64-bit MAXSIZE = int((1 << 63) - 1) # NOQA del X def sort_by_field(obj, field='name'): # NOQA def _by_field(item1, item2): return cmp(item1[field], item2[field]) # NOQA obj.sort(_by_field) else: import collections string_types = str integer_types = int text_type = str long = int unicode = str def sort_by_field(obj, field='name'): # NOQA def _by_field(item): return item[field] obj.sort(key=_by_field) def bytestring(s): # NOQA return s def cast_bytes(s, encoding='utf8'): # NOQA """cast unicode or bytes to bytes""" if isinstance(s, bytes): return s return str(s).encode(encoding) def cast_unicode(s, encoding='utf8', errors='replace'): # NOQA """cast bytes or unicode to unicode. errors options are strict, ignore or replace""" if isinstance(s, bytes): return s.decode(encoding, errors=errors) return str(s) cast_string = cast_unicode import io StringIO = io.StringIO # NOQA BytesIO = io.BytesIO # NOQA def raise_with_tb(E): # NOQA raise E.with_traceback(sys.exc_info()[2]) def is_callable(c): # NOQA return isinstance(c, collections.Callable) def get_next(c): # NOQA return c.__next__ MAXSIZE = sys.maxsize # NOQA b = cast_bytes s = cast_string u = cast_unicode try: # PY >= 3.3 from shlex import quote # NOQA except ImportError: from pipes import quote # NOQA circus-0.12.1/circus/sighandler.py000066400000000000000000000051351256046442300170570ustar00rootroot00000000000000import signal import traceback import sys from circus import logger from circus.client import make_json from circus.util import IS_WINDOWS class SysHandler(object): _SIGNALS_NAMES = ("ILL ABRT BREAK INT TERM" if IS_WINDOWS else "HUP QUIT INT TERM WINCH") SIGNALS = [getattr(signal, "SIG%s" % x) for x in _SIGNALS_NAMES.split()] SIG_NAMES = dict( (getattr(signal, name), name[3:].lower()) for name in dir(signal) if name[:3] == "SIG" and name[3] != "_" ) def __init__(self, controller): self.controller = controller # init signals logger.info('Registering signals...') self._old = {} self._register() def stop(self): for sig, callback in self._old.items(): try: signal.signal(sig, callback) except ValueError: pass def _register(self): for sig in self.SIGNALS: self._old[sig] = signal.getsignal(sig) signal.signal(sig, self.signal) # Don't let SIGQUIT and SIGUSR1 disturb active requests # by interrupting system calls if hasattr(signal, 'siginterrupt'): # python >= 2.6 signal.siginterrupt(signal.SIGQUIT, False) signal.siginterrupt(signal.SIGUSR1, False) def signal(self, sig, frame=None): signame = self.SIG_NAMES.get(sig) logger.info('Got signal SIG_%s' % signame.upper()) if signame is not None: try: handler = getattr(self, "handle_%s" % signame) handler() except AttributeError: pass except Exception as e: tb = traceback.format_exc() logger.error("error: %s [%s]" % (e, tb)) sys.exit(1) def quit(self): # We need to transfer the control to the loop's thread self.controller.loop.add_callback_from_signal( self.controller.dispatch, (None, make_json("quit")) ) def reload(self): # We need to transfer the control to the loop's thread self.controller.loop.add_callback_from_signal( self.controller.dispatch, (None, make_json("reload", graceful=True)) ) def handle_int(self): self.quit() def handle_term(self): self.quit() def handle_quit(self): self.quit() def handle_ill(self): self.quit() def handle_abrt(self): self.quit() def handle_break(self): self.quit() def handle_winch(self): pass def handle_hup(self): self.reload() circus-0.12.1/circus/sockets.py000066400000000000000000000260541256046442300164150ustar00rootroot00000000000000import socket import os from circus import logger from circus.util import papa, to_bool _FAMILY = { 'AF_INET': socket.AF_INET, 'AF_INET6': socket.AF_INET6 } if hasattr(socket, 'AF_UNIX'): _FAMILY['AF_UNIX'] = socket.AF_UNIX _TYPE = { 'SOCK_STREAM': socket.SOCK_STREAM, 'SOCK_DGRAM': socket.SOCK_DGRAM, 'SOCK_RAW': socket.SOCK_RAW, 'SOCK_RDM': socket.SOCK_RDM, 'SOCK_SEQPACKET': socket.SOCK_SEQPACKET } def addrinfo(host, port, family): for _addrinfo in socket.getaddrinfo(host, port): if len(_addrinfo[-1]) == 2: return _addrinfo[-1][-2], _addrinfo[-1][-1] if family == socket.AF_INET6 and len(_addrinfo[-1]) == 4: return _addrinfo[-1][-4], _addrinfo[-1][-3] raise ValueError((host, port)) class PapaSocketProxy(object): def __init__(self, name='', host=None, port=None, family=None, type=None, proto=None, backlog=None, path=None, umask=None, replace=None, interface=None, so_reuseport=False): if path is not None: if not hasattr(socket, 'AF_UNIX'): raise NotImplementedError("AF_UNIX not supported on this" " platform") else: family = socket.AF_UNIX host = port = None log_differences = False with papa.Papa() as p: prefixed_name = 'circus.' + name.lower() try: papa_socket = p.make_socket(prefixed_name, host, port, family, type, backlog, path, umask, interface, so_reuseport) except papa.Error: papa_socket = p.list_sockets(prefixed_name) if papa_socket: papa_socket = papa_socket[prefixed_name] log_differences = True else: raise self.name = name self.host = papa_socket.get('host') self.port = papa_socket.get('port') self.family = papa_socket['family'] self.socktype = papa_socket['type'] self.backlog = papa_socket.get('backlog') self.path = papa_socket.get('path') self.umask = papa_socket.get('umask') self.interface = papa_socket.get('interface') self.so_reuseport = papa_socket.get('so_reuseport', False) self._fileno = papa_socket.get('fileno') self.use_papa = True if log_differences: differences = [] if host != self.host: differences.append('host={0}'.format(self.host)) if port != self.port: differences.append('port={0}'.format(self.port)) if backlog != self.backlog: differences.append('backlog={0}'.format(self.backlog)) if path != self.path: differences.append('path={0}'.format(self.path)) if umask != self.umask: differences.append('umask={0}'.format(self.umask)) if interface != self.interface: differences.append('interface={0}'.format(self.interface)) if so_reuseport != self.so_reuseport: differences.append('so_reuseport={0}'.format( self.so_reuseport)) if differences: logger.warning('Socket "%s" already exists in papa with ' '%s. Using the existing socket.', name, ' '.join(differences)) self.replace = True def fileno(self): return self._fileno @property def location(self): if self.path: return '%r' % self.path return '%s:%d' % (self.host, self.port) def __str__(self): return 'socket %r at %s' % (self.name, self.location) def close(self): pass # papa manages the lifetime of these def bind_and_listen(self): pass # handled by papa class CircusSocket(socket.socket): """Inherits from socket, to add a few extra options. """ def __init__(self, name='', host='localhost', port=8080, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, backlog=2048, path=None, umask=None, replace=False, interface=None, so_reuseport=False): if path is not None: if not hasattr(socket, 'AF_UNIX'): raise NotImplementedError("AF_UNIX not supported on this" " platform") else: family = socket.AF_UNIX super(CircusSocket, self).__init__(family=family, type=type, proto=proto) self.name = name self.socktype = type self.path = path self.umask = umask self.replace = replace self.use_papa = False if hasattr(socket, 'AF_UNIX') and family == socket.AF_UNIX: self.host = self.port = None self.is_unix = True else: self.host, self.port = addrinfo(host, port, family) self.is_unix = False self.interface = interface self.backlog = backlog self.so_reuseport = so_reuseport if self.so_reuseport and hasattr(socket, 'SO_REUSEPORT'): try: self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except socket.error: # see 699 pass else: self.so_reuseport = False self.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Since python 3.4, file descriptors inheritance for children processes # is not the default anymore (#787) if hasattr(self, 'set_inheritable'): self.set_inheritable(True) @property def location(self): if self.is_unix: return '%r' % self.path return '%s:%d' % (self.host, self.port) def __str__(self): return 'socket %r at %s' % (self.name, self.location) def close(self): super(CircusSocket, self).close() if self.is_unix and os.path.exists(self.path): os.remove(self.path) def bind_and_listen(self): try: if self.is_unix: if os.path.exists(self.path): if self.replace: os.unlink(self.path) else: raise OSError("%r already exists. You might want to " "remove it. If it's a stalled socket " "file, just restart Circus" % self.path) if self.umask is None: self.bind(self.path) else: old_mask = os.umask(self.umask) self.bind(self.path) os.umask(old_mask) else: if self.interface is not None: # Bind to device if given, e.g. to limit which device to # bind when binding on IN_ADDR_ANY or IN_ADDR_BROADCAST. import IN if hasattr(IN, 'SO_BINDTODEVICE'): self.setsockopt(socket.SOL_SOCKET, IN.SO_BINDTODEVICE, self.interface + '\0') logger.debug('Binding to device: %s' % self.interface) self.bind((self.host, self.port)) except socket.error: logger.error('Could not bind %s' % self.location) raise self.setblocking(0) if self.socktype in (socket.SOCK_STREAM, socket.SOCK_SEQPACKET): self.listen(self.backlog) if not self.is_unix: if self.family == socket.AF_INET6: self.host, self.port, _flowinfo, _scopeid = self.getsockname() else: self.host, self.port = self.getsockname() logger.debug('Socket bound at %s - fd: %d' % (self.location, self.fileno())) @classmethod def load_from_config(cls, config): if (config.get('family') == 'AF_UNIX' and not hasattr(socket, 'AF_UNIX')): raise NotImplementedError("AF_UNIX not supported on this" "platform") params = {'name': config['name'], 'host': config.get('host', 'localhost'), 'port': int(config.get('port', '8080')), 'path': config.get('path'), 'interface': config.get('interface', None), 'family': _FAMILY[config.get('family', 'AF_INET').upper()], 'type': _TYPE[config.get('type', 'SOCK_STREAM').upper()], 'backlog': int(config.get('backlog', 2048)), 'so_reuseport': to_bool(config.get('so_reuseport')), 'umask': int(config.get('umask', 8)), 'replace': config.get('replace')} use_papa = to_bool(config.get('use_papa')) and papa is not None proto_name = config.get('proto') if proto_name is not None: params['proto'] = socket.getprotobyname(proto_name) socket_class = PapaSocketProxy if use_papa else cls s = socket_class(**params) # store the config for later checking if config has changed s._cfg = config.copy() return s class CircusSockets(dict): """Manage CircusSockets objects. """ def __init__(self, sockets=None, backlog=2048): super(CircusSockets, self).__init__() self.backlog = backlog if sockets is not None: for sock in sockets: self[sock.name] = sock def add(self, name, host='localhost', port=8080, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, backlog=None, path=None, umask=None, interface=None, use_papa=False): if backlog is None: backlog = self.backlog sock = self.get(name) if sock is not None: raise ValueError('A socket already exists %s' % sock) socket_class = PapaSocketProxy if use_papa else CircusSocket sock = socket_class(name=name, host=host, port=port, family=family, type=type, proto=proto, backlog=backlog, path=path, umask=umask, interface=interface) self[name] = sock return sock def close_all(self): papa_sockets = 0 for sock in self.values(): sock.close() if isinstance(sock, PapaSocketProxy): papa_sockets += 1 if papa_sockets: with papa.Papa() as p: procs = p.list_processes('circus.*') if not procs: logger.info('removing all papa sockets') p.remove_sockets('circus.*') if p.exit_if_idle(): logger.info('closing papa') def bind_and_listen_all(self): for sock in self.values(): # so_reuseport sockets should not be bound at this point if not sock.so_reuseport: sock.bind_and_listen() circus-0.12.1/circus/stats/000077500000000000000000000000001256046442300155175ustar00rootroot00000000000000circus-0.12.1/circus/stats/__init__.py000066400000000000000000000045471256046442300176420ustar00rootroot00000000000000 """ Stats architecture: * streamer.StatsStreamer listens to circusd events and maintain a list of pids * collector.StatsCollector runs a pool of threads that compute stats for each pid in the list. Each stat is pushed in a queue * publisher.StatsPublisher continuously pushes those stats in a zmq PUB socket * client.StatsClient is a simple subscriber that can be used to intercept the stream of stats. """ import sys import signal import argparse from circus.stats.streamer import StatsStreamer from circus.util import configure_logger from circus.sighandler import SysHandler from circus import logger from circus import util from circus import __version__ def main(): desc = 'Runs the stats aggregator for Circus' parser = argparse.ArgumentParser(description=desc) parser.add_argument('--endpoint', help='The circusd ZeroMQ socket to connect to', default=util.DEFAULT_ENDPOINT_DEALER) parser.add_argument('--pubsub', help='The circusd ZeroMQ pub/sub socket to connect to', default=util.DEFAULT_ENDPOINT_SUB) parser.add_argument('--statspoint', help='The ZeroMQ pub/sub socket to send data to', default=util.DEFAULT_ENDPOINT_STATS) parser.add_argument('--log-level', dest='loglevel', default='info', help="log level") parser.add_argument('--log-output', dest='logoutput', default='-', help="log output") parser.add_argument('--version', action='store_true', default=False, help='Displays Circus version and exits.') parser.add_argument('--ssh', default=None, help='SSH Server') args = parser.parse_args() if args.version: print(__version__) sys.exit(0) # configure the logger configure_logger(logger, args.loglevel, args.logoutput) stats = StatsStreamer(args.endpoint, args.pubsub, args.statspoint, args.ssh) # Register some sighandlers to stop the loop when killed for sig in SysHandler.SIGNALS: signal.signal( sig, lambda *_: stats.loop.add_callback_from_signal(stats.stop) ) try: stats.start() finally: stats.stop() sys.exit(0) if __name__ == '__main__': main() circus-0.12.1/circus/stats/client.py000066400000000000000000000171701256046442300173550ustar00rootroot00000000000000import argparse import sys import curses from collections import defaultdict import errno import circus.fixed_threading as threading import time import logging import zmq import zmq.utils.jsonapi as json from circus.consumer import CircusConsumer from circus import __version__ from circus.util import DEFAULT_ENDPOINT_STATS from circus.py3compat import s class StatsClient(CircusConsumer): def __init__(self, endpoint=DEFAULT_ENDPOINT_STATS, ssh_server=None, context=None): CircusConsumer.__init__(self, ['stat.'], context, endpoint, ssh_server) def iter_messages(self): """ Yields tuples of (watcher, subtopic, stat)""" recv = self.pubsub_socket.recv_multipart with self: while True: try: events = dict(self.poller.poll(self.timeout * 1000)) except zmq.ZMQError as e: if e.errno == errno.EINTR: continue raise if len(events) == 0: continue try: topic, stat = recv() except zmq.core.error.ZMQError as e: if e.errno != errno.EINTR: raise else: try: sys.exc_clear() except Exception: pass continue topic = s(topic).split('.') if len(topic) == 3: __, watcher, subtopic = topic yield watcher, subtopic, json.loads(stat) elif len(topic) == 2: __, watcher = topic yield watcher, None, json.loads(stat) def _paint(stdscr, watchers=None, old_h=None, old_w=None): current_h, current_w = stdscr.getmaxyx() def addstr(x, y, text): text_len = len(text) if x < current_h: padding = current_w - y if text_len >= padding: text = text[:padding - 1] else: text += ' ' * (padding - text_len - 1) if text == '': return stdscr.addstr(x, y, text) stdscr.erase() if watchers is None: stdscr.erase() addstr(1, 0, '*** Waiting for data ***') stdscr.refresh() return current_h, current_w if current_h != old_h or current_w != old_w: # we need a resize curses.endwin() stdscr.refresh() stdscr.erase() stdscr.resize(current_h, current_w) addstr(0, 0, 'Circus Top') addstr(1, 0, '-' * current_w) names = sorted(watchers.keys()) line = 2 for name in names: if name in ('circusd-stats', 'circushttpd'): continue addstr(line, 0, name.replace('-', '.')) line += 1 if name == 'sockets': addstr(line, 3, 'ADDRESS') addstr(line, 28, 'HITS') line += 1 fds = [] total = 0 for stats in watchers[name].values(): if 'addresses' in stats: total = stats['reads'] continue reads = stats['reads'] address = stats['address'] fds.append((reads, address)) fds.sort() fds.reverse() for reads, address in fds: addstr(line, 2, str(address)) addstr(line, 29, '%3d' % reads) line += 1 addstr(line, 29, '%3d (sum)' % total) line += 2 else: addstr(line, 3, 'PID') addstr(line, 28, 'CPU (%)') addstr(line, 48, 'MEMORY (%)') addstr(line, 68, 'AGE (s)') line += 1 # sorting by CPU pids = [] total = '', 'N/A', 'N/A', None for pid, stat in watchers[name].items(): if stat['cpu'] == 'N/A': cpu = 'N/A' else: cpu = "%.2f" % stat['cpu'] if stat['mem'] == 'N/A': mem = 'N/A' else: mem = "%.2f" % stat['mem'] if stat['age'] == 'N/A': age = 'N/A' else: age = "%.2f" % stat['age'] if pid == 'all' or isinstance(pid, list): total = (cpu + ' (avg)', mem + ' (sum)', age + ' (older)', '', None) else: pids.append((cpu, mem, age, str(stat['pid']), stat['name'])) pids.sort() pids.reverse() pids = pids[:10] + [total] for cpu, mem, age, pid, name in pids: if name is not None: pid = '%s (%s)' % (pid, name) addstr(line, 2, pid) addstr(line, 29, cpu) addstr(line, 49, mem) addstr(line, 69, age) line += 1 line += 1 if line < current_h and len(watchers) > 0: addstr(line, 0, '-' * current_w) stdscr.refresh() return current_h, current_w class Painter(threading.Thread): def __init__(self, screen, watchers, h, w): threading.Thread.__init__(self) self.daemon = True self.screen = screen self.watchers = watchers self.running = False self.h = h self.w = w def stop(self): self.running = False def run(self): self.running = True while self.running: self.h, self.w = _paint(self.screen, self.watchers, self.h, self.w) time.sleep(1.) def main(): logging.basicConfig() desc = 'Runs Circus Top' parser = argparse.ArgumentParser(description=desc) parser.add_argument('--endpoint', help='The circusd-stats ZeroMQ socket to connect to', default=DEFAULT_ENDPOINT_STATS) parser.add_argument('--version', action='store_true', default=False, help='Displays Circus version and exits.') parser.add_argument('--ssh', default=None, help='SSH Server') parser.add_argument('--process-timeout', default=3, help='After this delay of inactivity, a process will \ be removed') args = parser.parse_args() if args.version: print(__version__) sys.exit(0) stdscr = curses.initscr() watchers = defaultdict(dict) h, w = _paint(stdscr) last_refresh_for_pid = defaultdict(float) time.sleep(1.) painter = Painter(stdscr, watchers, h, w) painter.start() try: client = StatsClient(args.endpoint, args.ssh) try: for watcher, subtopic, stat in client: # building the line stat['watcher'] = watcher if subtopic is None: subtopic = 'all' # Clean pids that have not been updated recently for pid in tuple(p for p in watchers[watcher] if p.isdigit()): if (last_refresh_for_pid[pid] < time.time() - int(args.process_timeout)): del watchers[watcher][pid] last_refresh_for_pid[subtopic] = time.time() # adding it to the structure watchers[watcher][subtopic] = stat except KeyboardInterrupt: client.stop() finally: painter.stop() curses.endwin() if __name__ == '__main__': main() circus-0.12.1/circus/stats/collector.py000066400000000000000000000130541256046442300200620ustar00rootroot00000000000000import errno from collections import defaultdict import select import socket from circus import util from circus import logger from zmq.eventloop import ioloop class BaseStatsCollector(ioloop.PeriodicCallback): def __init__(self, streamer, name, callback_time=1., io_loop=None): ioloop.PeriodicCallback.__init__(self, self._callback, callback_time * 1000, io_loop) self.streamer = streamer self.name = name def _callback(self): logger.debug('Publishing stats about {0}'.format(self.name)) for stats in self.collect_stats(): if stats is None: continue self.streamer.publisher.publish(self.name, stats) def collect_stats(self): # should be implemented in subclasses raise NotImplementedError() # PRAGMA: NOCOVER class WatcherStatsCollector(BaseStatsCollector): def _aggregate(self, aggregate): res = {'pid': list(aggregate.keys())} stats = list(aggregate.values()) # aggregating CPU does not mean anything # but the average can be a good indicator cpu = [stat['cpu'] for stat in stats] if 'N/A' in cpu: res['cpu'] = 'N/A' else: try: res['cpu'] = sum(cpu) / len(cpu) except ZeroDivisionError: res['cpu'] = 0. # aggregating memory does make sense mem = [stat['mem'] for stat in stats] if 'N/A' in mem: res['mem'] = 'N/A' else: res['mem'] = sum(mem) # finding out the older process ages = [stat['age'] for stat in stats if stat['age'] != 'N/A'] if len(ages) == 0: res['age'] = 'N/A' else: res['age'] = max(ages) return res def collect_stats(self): aggregate = {} # sending by pids for pid in self.streamer.get_pids(self.name): name = None if self.name == 'circus': if pid in self.streamer.circus_pids: name = self.streamer.circus_pids[pid] try: info = util.get_info(pid) aggregate[pid] = info info['subtopic'] = pid info['name'] = name yield info except util.NoSuchProcess: # the process is gone ! pass except Exception as e: logger.exception('Failed to get info for %d. %s' % (pid, str(e))) # now sending the aggregation yield self._aggregate(aggregate) # RESOLUTION is a value in seconds that will be used # to determine the poller timeout of the sockets stats collector # # The PeriodicCallback calls the poller every LOOP_RES ms, and block # for RESOLUTION seconds unless a read ready event occurs in the # socket. # # This timer is used to limit the number of polls done on the # socket, so the circusd-stats process don't eat all your CPU # when you have a high-loaded socket. # _RESOLUTION = .1 _LOOP_RES = 10 class SocketStatsCollector(BaseStatsCollector): def __init__(self, streamer, name, callback_time=1., io_loop=None): super(SocketStatsCollector, self).__init__(streamer, name, callback_time, io_loop) self._rstats = defaultdict(int) self.sockets = [sock for sock, address, fd in self.streamer.sockets] self._p = ioloop.PeriodicCallback(self._select, _LOOP_RES, io_loop=io_loop) def start(self): self._p.start() super(SocketStatsCollector, self).start() def stop(self): self._p.stop() BaseStatsCollector.stop(self) def _select(self): try: rlist, wlist, xlist = select.select(self.sockets, [], [], .01) except socket.error as err: if err.errno in (errno.EBADF, errno.EINTR): return raise except select.error as err: if err.args[0] == errno.EINTR: return if len(rlist) == 0: return for sock in rlist: try: fileno = sock.fileno() except socket.error as err: if err.errno == errno.EBADF: continue else: raise self._rstats[fileno] += 1 def _aggregate(self, aggregate): raise NotImplementedError() def collect_stats(self): # sending hits by sockets sockets = self.streamer.sockets if len(sockets) == 0: yield None else: fds = [] for sock, address, fd in sockets: try: fileno = sock.fileno() except socket.error as err: if err.errno == errno.EBADF: continue else: raise fds.append((address, fileno, fd)) total = {'addresses': [], 'reads': 0} # we might lose a few hits here but it's ok for address, monitored_fd, fd in fds: info = {} info['fd'] = info['subtopic'] = fd info['reads'] = self._rstats[monitored_fd] total['reads'] += info['reads'] total['addresses'].append(address) info['address'] = address self._rstats[monitored_fd] = 0 yield info yield total circus-0.12.1/circus/stats/publisher.py000066400000000000000000000020211256046442300200610ustar00rootroot00000000000000import zmq import zmq.utils.jsonapi as json from circus.py3compat import b from circus import logger class StatsPublisher(object): def __init__(self, stats_endpoint='tcp://127.0.0.1:5557', context=None): self.ctx = context or zmq.Context() self.destroy_context = context is None self.stats_endpoint = stats_endpoint self.socket = self.ctx.socket(zmq.PUB) self.socket.bind(self.stats_endpoint) self.socket.linger = 0 def publish(self, name, stat): try: topic = 'stat.%s' % str(name) if 'subtopic' in stat: topic += '.%d' % stat['subtopic'] stat = json.dumps(stat) logger.debug('Sending %s' % stat) self.socket.send_multipart([b(topic), stat]) except zmq.ZMQError: if self.socket.closed: pass else: raise def stop(self): if self.destroy_context: self.ctx.destroy(0) logger.debug('Publisher stopped') circus-0.12.1/circus/stats/streamer.py000066400000000000000000000167341256046442300177260ustar00rootroot00000000000000from collections import defaultdict from itertools import chain import os import errno import socket import zmq import zmq.utils.jsonapi as json from zmq.eventloop import ioloop, zmqstream from circus.commands import get_commands from circus.client import CircusClient from circus.stats.collector import WatcherStatsCollector, SocketStatsCollector from circus.stats.publisher import StatsPublisher from circus import logger from circus.py3compat import s class StatsStreamer(object): def __init__(self, endpoint, pubsub_endoint, stats_endpoint, ssh_server=None, delay=1., loop=None): self.topic = b'watcher.' self.delay = delay self.ctx = zmq.Context() self.pubsub_endpoint = pubsub_endoint self.sub_socket = self.ctx.socket(zmq.SUB) self.sub_socket.setsockopt(zmq.SUBSCRIBE, self.topic) self.sub_socket.connect(self.pubsub_endpoint) self.loop = loop or ioloop.IOLoop.instance() self.substream = zmqstream.ZMQStream(self.sub_socket, self.loop) self.substream.on_recv(self.handle_recv) self.client = CircusClient(context=self.ctx, endpoint=endpoint, ssh_server=ssh_server) self.cmds = get_commands() self.publisher = StatsPublisher(stats_endpoint, self.ctx) self._initialize() def _initialize(self): self._pids = defaultdict(list) self._callbacks = dict() self.running = False # should the streamer be running? self.stopped = False # did the collect started yet? self.circus_pids = {} self.sockets = [] self.get_watchers = self._pids.keys def get_pids(self, watcher=None): if watcher is not None: if watcher == 'circus': return list(self.circus_pids.keys()) return self._pids[watcher] return chain(*list(self._pids.values())) def get_circus_pids(self): watchers = self.client.send_message('list').get('watchers', []) # getting the circusd, circusd-stats and circushttpd pids res = self.client.send_message('dstats') pids = {os.getpid(): 'circusd-stats'} if 'info' in res: pids[res['info']['pid']] = 'circusd' if 'circushttpd' in watchers: httpd_pids = self.client.send_message('list', name='circushttpd') if 'pids' in httpd_pids: httpd_pids = httpd_pids['pids'] if len(httpd_pids) == 1: pids[httpd_pids[0]] = 'circushttpd' return pids def _add_callback(self, name, start=True, kind='watcher'): logger.debug('Callback added for %s' % name) if kind == 'watcher': klass = WatcherStatsCollector elif kind == 'socket': klass = SocketStatsCollector else: raise ValueError('Unknown callback kind %r' % kind) self._callbacks[name] = klass(self, name, self.delay, self.loop) if start: self._callbacks[name].start() def _init(self): self._pids.clear() # getting the initial list of watchers/pids res = self.client.send_message('list') for watcher in res['watchers']: if watcher in ('circusd', 'circushttpd', 'circusd-stats'): # this is dealt by the special 'circus' collector continue pid_list = self.client.send_message('list', name=watcher) pids = pid_list.get('pids', []) for pid in pids: self._append_pid(watcher, pid) # getting the circus pids self.circus_pids = self.get_circus_pids() if 'circus' not in self._callbacks: self._add_callback('circus') else: self._callbacks['circus'].start() # getting the initial list of sockets res = self.client.send_message('listsockets') for sock in res.get('sockets', []): fd = sock['fd'] if 'path' in sock: # unix socket address = sock['path'] else: address = '%s:%s' % (sock['host'], sock['port']) # XXX type / family ? sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) self.sockets.append((sock, address, fd)) self._add_callback('sockets', kind='socket') def stop_watcher(self, watcher): for pid in self._pids[watcher]: self.remove_pid(watcher, pid) def remove_pid(self, watcher, pid): if pid in self._pids[watcher]: logger.debug('Removing %d from %s' % (pid, watcher)) self._pids[watcher].remove(pid) if len(self._pids[watcher]) == 0: logger.debug( 'Stopping the periodic callback for {0}' .format(watcher)) self._callbacks[watcher].stop() def _append_pid(self, watcher, pid): if watcher not in self._pids or len(self._pids[watcher]) == 0: logger.debug( 'Starting the periodic callback for {0}'.format(watcher)) if watcher not in self._callbacks: self._add_callback(watcher) else: self._callbacks[watcher].start() if pid in self._pids[watcher]: return self._pids[watcher].append(pid) logger.debug('Adding %d in %s' % (pid, watcher)) def start(self): self.running = True logger.info('Starting the stats streamer') self._init() logger.debug('Initial list is ' + str(self._pids)) logger.debug('Now looping to get circusd events') while self.running: try: self.loop.start() except zmq.ZMQError as e: logger.debug(str(e)) if e.errno == errno.EINTR: continue elif e.errno == zmq.ETERM: break else: logger.debug("got an unexpected error %s (%s)", str(e), e.errno) raise else: break self.stop() def handle_recv(self, data): """called each time circusd sends an event""" # maintains a periodic callback to compute mem and cpu consumption for # each pid. logger.debug('Received an event from circusd: %s' % str(data)) topic, msg = data try: topic = s(topic) watcher = topic.split('.')[1:-1][0] action = topic.split('.')[-1] msg = json.loads(msg) if action in ('reap', 'kill'): # a process was reaped pid = msg['process_pid'] self.remove_pid(watcher, pid) elif action == 'spawn': # a process was added pid = msg['process_pid'] self._append_pid(watcher, pid) elif action == 'stop': # the whole watcher was stopped. self.stop_watcher(watcher) else: logger.debug('Unknown action: %r' % action) logger.debug(msg) except Exception: logger.exception('Failed to handle %r' % msg) def stop(self): # stop all the periodic callbacks running for callback in self._callbacks.values(): callback.stop() self.loop.stop() self.ctx.destroy(0) self.publisher.stop() self.stopped = True self.running = False logger.info('Stats streamer stopped') circus-0.12.1/circus/stream/000077500000000000000000000000001256046442300156545ustar00rootroot00000000000000circus-0.12.1/circus/stream/__init__.py000066400000000000000000000073741256046442300200000ustar00rootroot00000000000000import sys import random from datetime import datetime try: from queue import Queue, Empty except ImportError: from Queue import Queue, Empty # NOQA from circus.util import resolve_name from circus.stream.file_stream import FileStream from circus.stream.file_stream import WatchedFileStream # flake8: noqa from circus.stream.file_stream import TimedRotatingFileStream # flake8: noqa from circus.stream.redirector import Redirector from circus.py3compat import s class QueueStream(Queue): def __init__(self, **kwargs): Queue.__init__(self) def __call__(self, data): self.put(data) def close(self): pass class StdoutStream(object): def __init__(self, **kwargs): pass def __call__(self, data): sys.stdout.write(s(data['data'])) sys.stdout.flush() def close(self): pass class FancyStdoutStream(StdoutStream): """ Write output from watchers using different colors along with a timestamp. If no color is selected a color will be chosen at random. The available ascii colors are: - red - green - yellow - blue - magenta - cyan - white You may also configure the timestamp format as defined by datetime.strftime. The default is: :: %Y-%m-%d %H:%M:%S Here is an example: :: [watcher:foo] cmd = python -m myapp.server stdout_stream.class = FancyStdoutStream stdout_stream.color = green stdout_stream.time_format = '%Y/%m/%d | %H:%M:%S' """ # colors in order according to the ascii escape sequences colors = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'] # Where we write output out = sys.stdout # Generate a datetime object now = datetime.now fromtimestamp = datetime.fromtimestamp def __init__(self, color=None, time_format=None, **kwargs): super(FancyStdoutStream, self).__init__(**kwargs) self.time_format = time_format or '%Y-%m-%d %H:%M:%S' if color not in self.colors: color = random.choice(self.colors) self.color_code = self.colors.index(color) + 1 def prefix(self, data): """ Create a prefix for each line. This includes the ansi escape sequence for the color. This will not work on windows. For something more robust there is a good discussion over on Stack Overflow: http://stackoverflow.com/questions/287871 """ pid = data['pid'] if 'timestamp' in data: time = self.fromtimestamp(data['timestamp']) else: time = self.now() time = time.strftime(self.time_format) # start the coloring with the ansi escape sequence color = '\033[0;3%s;40m' % self.color_code prefix = '{time} [{pid}] | '.format(pid=pid, time=time) return color + prefix def __call__(self, data): for line in s(data['data']).split('\n'): if line: self.out.write(self.prefix(data)) self.out.write(line) # stop coloring self.out.write('\033[0m\n') self.out.flush() def get_stream(conf, reload=False): if conf: # we can have 'stream' or 'class' or 'filename' if 'class' in conf: class_name = conf.pop('class') if not "." in class_name: cls = globals()[class_name] inst = cls(**conf) else: inst = resolve_name(class_name, reload=reload)(**conf) elif 'stream' in conf: inst = conf['stream'] elif 'filename' in conf: inst = FileStream(**conf) else: raise ValueError("stream configuration invalid") return inst circus-0.12.1/circus/stream/file_stream.py000066400000000000000000000333411256046442300205240ustar00rootroot00000000000000import errno import os import tempfile from datetime import datetime import time as time_ import re from stat import ST_DEV, ST_INO, ST_MTIME from circus import logger from circus.py3compat import s, PY2 class _FileStreamBase(object): """Base class for all file writer handler classes""" # You may want to use another now or fromtimestamp method # (not naive or a mock). now = datetime.now fromtimestamp = datetime.fromtimestamp def __init__(self, filename, time_format): if filename is None: fd, filename = tempfile.mkstemp() os.close(fd) self._filename = filename self._file = self._open() self._time_format = time_format self._buffer = [] # XXX - is this really needed? def _open(self): return open(self._filename, 'a+') def open(self): if self._file.closed: self._file = self._open() def close(self): self._file.close() def write_data(self, data): # data to write on file file_data = s(data['data']) # If we want to prefix the stream with the current datetime if self._time_format is not None: if 'timestamp' in data: time = self.fromtimestamp(data['timestamp']) else: time = self.now() time = time.strftime(self._time_format) prefix = '{time} [{pid}] | '.format(time=time, pid=data['pid']) file_data = prefix + file_data.rstrip('\n') file_data = file_data.replace('\n', '\n' + prefix) file_data += '\n' # writing into the file try: self._file.write(file_data) except Exception: # we can strip the string down on Py3 but not on Py2 if not PY2: file_data = file_data.encode('latin-1', errors='replace') file_data = file_data.decode('latin-1') self._file.write(file_data) else: raise self._file.flush() class FileStream(_FileStreamBase): def __init__(self, filename=None, max_bytes=0, backup_count=0, time_format=None, **kwargs): ''' File writer handler which writes output to a file, allowing rotation behaviour based on Python's ``logging.handlers.RotatingFileHandler``. By default, the file grows indefinitely. You can specify particular values of max_bytes and backup_count to allow the file to rollover at a predetermined size. Rollover occurs whenever the current log file is nearly max_bytes in length. If backup_count is >= 1, the system will successively create new files with the same pathname as the base file, but with extensions ".1", ".2" etc. appended to it. For example, with a backup_count of 5 and a base file name of "app.log", you would get "app.log", "app.log.1", "app.log.2", ... through to "app.log.5". The file being written to is always "app.log" - when it gets filled up, it is closed and renamed to "app.log.1", and if files "app.log.1", "app.log.2" etc. exist, then they are renamed to "app.log.2", "app.log.3" etc. respectively. If max_bytes is zero, rollover never occurs. You may also configure the timestamp format as defined by datetime.strftime. Here is an example: :: [watcher:foo] cmd = python -m myapp.server stdout_stream.class = FileStream stdout_stream.filename = /var/log/circus/out.log stdout_stream.time_format = %Y-%m-%d %H:%M:%S ''' super(FileStream, self).__init__(filename, time_format) self._max_bytes = int(max_bytes) self._backup_count = int(backup_count) def __call__(self, data): if self._should_rollover(data['data']): self._do_rollover() self.write_data(data) def _do_rollover(self): """ Do a rollover, as described in __init__(). """ if self._file: self._file.close() self._file = None if self._backup_count > 0: for i in range(self._backup_count - 1, 0, -1): sfn = "%s.%d" % (self._filename, i) dfn = "%s.%d" % (self._filename, i + 1) if os.path.exists(sfn): logger.debug("Log rotating %s -> %s" % (sfn, dfn)) if os.path.exists(dfn): os.remove(dfn) os.rename(sfn, dfn) dfn = self._filename + ".1" if os.path.exists(dfn): os.remove(dfn) os.rename(self._filename, dfn) logger.debug("Log rotating %s -> %s" % (self._filename, dfn)) self._file = self._open() def _should_rollover(self, raw_data): """ Determine if rollover should occur. Basically, see if the supplied raw_data would cause the file to exceed the size limit we have. """ if self._file is None: # delay was set... self._file = self._open() if self._max_bytes > 0: # are we rolling over? self._file.seek(0, 2) # due to non-posix-compliant Windows feature if self._file.tell() + len(raw_data) >= self._max_bytes: return 1 return 0 class WatchedFileStream(_FileStreamBase): def __init__(self, filename=None, time_format=None, **kwargs): ''' File writer handler which writes output to a file, allowing an external log rotation process to handle rotation, like Python's ``logging.handlers.WatchedFileHandler``. By default, the file grows indefinitely, and you are responsible for ensuring that log rotation happens with some external tool like logrotate. You may also configure the timestamp format as defined by datetime.strftime. Here is an example: :: [watcher:foo] cmd = python -m myapp.server stdout_stream.class = WatchedFileStream stdout_stream.filename = /var/log/circus/out.log stdout_stream.time_format = %Y-%m-%d %H:%M:%S ''' super(WatchedFileStream, self).__init__(filename, time_format) self.dev, self.ino = -1, -1 self._statfile() def _statfile(self): stb = os.fstat(self._file.fileno()) self.dev, self.ino = stb[ST_DEV], stb[ST_INO] def _statfilename(self): try: stb = os.stat(self._filename) return stb[ST_DEV], stb[ST_INO] except OSError as err: if err.errno == errno.ENOENT: return -1, -1 else: raise def __call__(self, data): # stat the filename to see if the file we opened still exists. If the # ino or dev doesn't match, we need to open a new file handle dev, ino = self._statfilename() if dev != self.dev or ino != self.ino: self._file.flush() self._file.close() self._file = self._open() self._statfile() self.write_data(data) _MIDNIGHT = 24 * 60 * 60 # number of seconds in a day class TimedRotatingFileStream(FileStream): def __init__(self, filename=None, backup_count=0, time_format=None, rotate_when=None, rotate_interval=1, utc=False, **kwargs): ''' File writer handler which writes output to a file, allowing rotation behaviour based on Python's ``logging.handlers.TimedRotatingFileHandler``. The parameters are the same as ``FileStream`` except max_bytes. In addition you can specify extra parameters: - utc: if True, times in UTC will be used. otherwise local time is used. Default: False. - rotate_when: the type of interval. Can be S, M, H, D, 'W0'-'W6' or 'midnight'. See Python's TimedRotatingFileHandler for more information. - rotate_interval: Rollover interval in seconds. Default: 1 Here is an example: :: [watcher:foo] cmd = python -m myapp.server stdout_stream.class = TimedRotatingFileStream stdout_stream.filename = /var/log/circus/out.log stdout_stream.time_format = %Y-%m-%d %H:%M:%S stdout_stream.utc = True stdout_stream.rotate_when = H stdout_stream.rotate_interval = 1 ''' super(TimedRotatingFileStream, self).__init__(filename=filename, backup_count=backup_count, time_format=time_format, utc=False, **kwargs) self._utc = bool(utc) self._when = rotate_when if self._when == "S": self._interval = 1 self._suffix = "%Y%m%d%H%M%S" self._ext_match = r"^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}$" elif self._when == "M": self._interval = 60 self._suffix = "%Y%m%d%H%M" self._ext_match = r"^\d{4}\d{2}\d{2}\d{2}\d{2}$" elif self._when == "H": self._interval = 60 * 60 self._suffix = "%Y%m%d%H" self._ext_match = r"^\d{4}\d{2}\d{2}\d{2}$" elif self._when in ("D", "MIDNIGHT"): self._interval = 60 * 60 * 24 self._suffix = "%Y%m%d" self._ext_match = r"^\d{4}\d{2}\d{2}$" elif self._when.startswith("W"): self._interval = 60 * 60 * 24 * 7 if len(self._when) != 2: raise ValueError("You must specify a day for weekly\ rollover from 0 to 6 (0 is Monday): %s" % self._when) if self._when[1] < "0" or self._when[1] > "6": raise ValueError("Invalid day specified\ for weekly rollover: %s" % self._when) self._day_of_week = int(self._when[1]) self._suffix = "%Y%m%d" self._ext_match = r"^\d{4}\d{2}\d{2}$" else: raise ValueError("Invalid rollover interval specified: %s" % self._when) self._ext_match = re.compile(self._ext_match) self._interval = self._interval * int(rotate_interval) if os.path.exists(self._filename): t = os.stat(self._filename)[ST_MTIME] else: t = int(time_.time()) self._rollover_at = self._compute_rollover(t) def _do_rollover(self): if self._file: self._file.close() self._file = None current_time = int(time_.time()) dst_now = time_.localtime(current_time)[-1] t = self._rollover_at - self._interval if self._utc: time_touple = time_.gmtime(t) else: time_touple = time_.localtime(t) dst_then = time_touple[-1] if dst_now != dst_then: if dst_now: addend = 3600 else: addend = -3600 time_touple = time_.localtime(t + addend) dfn = self._filename + "." + time_.strftime(self._suffix, time_touple) if os.path.exists(dfn): os.remove(dfn) if os.path.exists(self._filename): os.rename(self._filename, dfn) logger.debug("Log rotating %s -> %s" % (self._filename, dfn)) if self._backup_count > 0: for f in self._get_files_to_delete(): os.remove(f) self._file = self._open() new_rollover_at = self._compute_rollover(current_time) while new_rollover_at <= current_time: new_rollover_at = new_rollover_at + self._interval self._rollover_at = new_rollover_at def _compute_rollover(self, current_time): result = current_time + self._interval if self._when == "MIDNIGHT" or self._when.startswith("W"): if self._utc: t = time_.gmtime(current_time) else: t = time_.localtime(current_time) current_hour = t[3] current_minute = t[4] current_second = t[5] r = _MIDNIGHT - ((current_hour * 60 + current_minute) * 60 + current_second) result = current_time + r if self._when.startswith("W"): day = t[6] if day != self._day_of_week: days_to_wait = self._day_of_week - day else: days_to_wait = 6 - day + self._day_of_week + 1 new_rollover_at = result + (days_to_wait * (60 * 60 * 24)) if not self._utc: dst_now = t[-1] dst_at_rollover = time_.localtime(new_rollover_at)[-1] if dst_now != dst_at_rollover: if not dst_now: addend = -3600 else: addend = 3600 new_rollover_at += addend result = new_rollover_at return result def _get_files_to_delete(self): dirname, basename = os.path.split(self._filename) prefix = basename + "." plen = len(prefix) result = [] for filename in os.listdir(dirname): if filename[:plen] == prefix: suffix = filename[plen:] if self._ext_match.match(suffix): result.append(os.path.join(dirname, filename)) result.sort() if len(result) < self._backup_count: return [] return result[:len(result) - self._backup_count] def _should_rollover(self, raw_data): """ Determine if rollover should occur. record is not used, as we are just comparing times, but it is needed so the method signatures are the same """ t = int(time_.time()) if t >= self._rollover_at: return 1 return 0 circus-0.12.1/circus/stream/papa_redirector.py000066400000000000000000000032241256046442300213720ustar00rootroot00000000000000from zmq.eventloop import ioloop from circus.stream import Redirector class PapaRedirector(Redirector): class Handler(Redirector.Handler): def __init__(self, redirector, name, process, pipe): self.redirector = redirector self.name = name self.process = process self.pipe = pipe def __call__(self, fd, events): if not (events & ioloop.IOLoop.READ): if events == ioloop.IOLoop.ERROR: self.redirector.remove_fd(fd) return return self.do_read(fd) def do_read(self, fd): out, err, close = self.pipe.read() for output_type, output_list in (('stdout', out), ('stderr', err)): if output_list: for line in output_list: datamap = {'data': line.data, 'pid': self.process.pid, 'name': output_type, 'timestamp': line.timestamp} self.redirector.redirect[output_type](datamap) self.pipe.acknowledge() if close: self.redirector.remove_fd(fd) def stop(self): count = 0 for fd, handler in list(self._active.items()): # flush whatever is pending if handler.pipe.ready: handler.do_read(fd) count += self._stop_one(fd) self.running = False return count @staticmethod def get_process_pipes(process): if process.pipe_stdout or process.pipe_stderr: yield 'output', process.output circus-0.12.1/circus/stream/redirector.py000066400000000000000000000064531256046442300204000ustar00rootroot00000000000000import errno import os import sys from zmq.eventloop import ioloop class Redirector(object): class Handler(object): def __init__(self, redirector, name, process, pipe): self.redirector = redirector self.name = name self.process = process self.pipe = pipe def __call__(self, fd, events): if not (events & ioloop.IOLoop.READ): if events == ioloop.IOLoop.ERROR: self.redirector.remove_fd(fd) return try: data = os.read(fd, self.redirector.buffer) if len(data) == 0: self.redirector.remove_fd(fd) else: datamap = {'data': data, 'pid': self.process.pid, 'name': self.name} self.redirector.redirect[self.name](datamap) except IOError as ex: if ex.args[0] != errno.EAGAIN: raise try: sys.exc_clear() except Exception: pass def __init__(self, stdout_redirect, stderr_redirect, buffer=1024, loop=None): self.running = False self.pipes = {} self._active = {} self.redirect = {'stdout': stdout_redirect, 'stderr': stderr_redirect} self.buffer = buffer self.loop = loop or ioloop.IOLoop.instance() def _start_one(self, stream_name, process, pipe): fd = pipe.fileno() if fd not in self._active: handler = self.Handler(self, stream_name, process, pipe) self.loop.add_handler(fd, handler, ioloop.IOLoop.READ) self._active[fd] = handler return 1 return 0 def start(self): count = 0 for name, process, pipe in self.pipes.values(): count += self._start_one(name, process, pipe) self.running = True return count def _stop_one(self, fd): if fd in self._active: self.loop.remove_handler(fd) del self._active[fd] return 1 return 0 def stop(self): count = 0 for fd in list(self._active.keys()): count += self._stop_one(fd) self.running = False return count @staticmethod def get_process_pipes(process): if process.pipe_stdout: yield 'stdout', process.stdout if process.pipe_stderr: yield 'stderr', process.stderr def add_redirections(self, process): for name, pipe in self.get_process_pipes(process): fd = pipe.fileno() self._stop_one(fd) self.pipes[fd] = name, process, pipe if self.running: self._start_one(name, process, pipe) process.redirected = True def remove_fd(self, fd): self._stop_one(fd) if fd in self.pipes: del self.pipes[fd] def remove_redirections(self, process): for _, pipe in self.get_process_pipes(process): self.remove_fd(pipe.fileno()) process.redirected = False def change_stream(self, stream_name, redirect_writer): self.redirect[stream_name] = redirect_writer def get_stream(self, stream_name): return self.redirect.get(stream_name) circus-0.12.1/circus/tests/000077500000000000000000000000001256046442300155235ustar00rootroot00000000000000circus-0.12.1/circus/tests/NEED_LOVE.txt000066400000000000000000000001331256046442300176210ustar00rootroot00000000000000test_green.py (renamed _test_green.py) + some FIXME/TODO tests (a few in test_arbiter.py) circus-0.12.1/circus/tests/__init__.py000066400000000000000000000004431256046442300176350ustar00rootroot00000000000000import os from circus.util import configure_logger from circus import logger _CONFIGURED = False if not _CONFIGURED and 'TESTING' in os.environ: configure_logger(logger, level='CRITICAL', output=os.devnull) _CONFIGURED = True def setUp(): from circus import _patch # NOQA circus-0.12.1/circus/tests/config/000077500000000000000000000000001256046442300167705ustar00rootroot00000000000000circus-0.12.1/circus/tests/config/circus.ini000066400000000000000000000001021256046442300207520ustar00rootroot00000000000000[circus] pidfile = pidfile loglevel = debug logoutput = logoutput circus-0.12.1/circus/tests/config/copy_env.ini000066400000000000000000000006111256046442300213110ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 statsd = true [watcher:watcher1] cmd = boo graceful_timeout=30 [watcher:watcher2] cmd = boo copy_env = True graceful_timeout=30 [env:watcher1] CAKE = lie [env:watcher2] LIE = cake [env:watcher1,watcher2] PATH = $PATH:/bin [env] TEST1 = test1 [env:wat*] TEST2 = test2 [env:*] TEST3 = test3 circus-0.12.1/circus/tests/config/empty_include.ini000066400000000000000000000000641256046442300223320ustar00rootroot00000000000000[circus] include = garbage*.ini include_dir = empty circus-0.12.1/circus/tests/config/env_everywhere.ini000066400000000000000000000003561256046442300225320ustar00rootroot00000000000000[circus] endpoint = tcp://127.0.0.1:$(circus.env.ENDPOINT_PORT) [socket:bad] path = /var/run/$(circus.env.BAD_PATH) [plugin:breaking] use = bad.has.been.$(circus.env.WHAT) [env] ENDPOINT_PORT = 1234 BAD_PATH = broken.sock WHAT = brokencircus-0.12.1/circus/tests/config/env_section.ini000066400000000000000000000005711256046442300220100ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 statsd = true [watcher:watcher1] cmd = boo graceful_timeout=30 [watcher:watcher2] cmd = boo graceful_timeout=30 [env:watcher1] CAKE = lie [env:watcher2] LIE = cake [env:watcher1,watcher2] PATH = $PATH:/bin [env] TEST1 = test1 [env:wat*] TEST2 = test2 [env:*] TEST3 = test3 circus-0.12.1/circus/tests/config/env_sensecase.ini000066400000000000000000000004331256046442300223120ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = True [watcher:webapp] cmd = curl [env:webapp] http_proxy = http://localhost:8080 HTTPS_PROXY = http://localhost:8043 FunKy_soUl = scorpio circus-0.12.1/circus/tests/config/env_var.ini000066400000000000000000000003451256046442300211330ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = True [watcher:my_app] cmd = boo graceful_timeout = 30 [env:my_app] PATH = $PATH:/bincircus-0.12.1/circus/tests/config/expand_vars.ini000066400000000000000000000004311256046442300220010ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 [watcher:echo] cmd = echo args = hi stdout_stream.class = FileStream stdout_stream.filename = $(circus.env.LOGDIR)/echo.log stdout_stream.max_bytes = 10485760 [env:*] LOGDIR = /tmp circus-0.12.1/circus/tests/config/hooks.ini000066400000000000000000000001121256046442300206060ustar00rootroot00000000000000[watcher:foo] hooks.before_start = circus.tests.test_config.hook, false circus-0.12.1/circus/tests/config/include.ini000066400000000000000000000000711256046442300211120ustar00rootroot00000000000000[circus] include = included-*.ini include_dir = included circus-0.12.1/circus/tests/config/include_dir.ini000066400000000000000000000000421256046442300217460ustar00rootroot00000000000000[circus] include = included-*.ini circus-0.12.1/circus/tests/config/included-bar.ini000066400000000000000000000000571256046442300220240ustar00rootroot00000000000000[watcher:bar] cmd = sleep 120 numprocesses = 5 circus-0.12.1/circus/tests/config/included-foo.ini000066400000000000000000000000571256046442300220430ustar00rootroot00000000000000[watcher:foo] cmd = sleep 120 numprocesses = 5 circus-0.12.1/circus/tests/config/included/000077500000000000000000000000001256046442300205575ustar00rootroot00000000000000circus-0.12.1/circus/tests/config/included/barbaz.ini000066400000000000000000000001141256046442300225150ustar00rootroot00000000000000[circus] check_delay = 2 [watcher:barbaz] cmd = sleep 120 numprocesses = 5 circus-0.12.1/circus/tests/config/included/foobar.ini000066400000000000000000000001221256046442300225230ustar00rootroot00000000000000[watcher:foobar] cmd = sleep 120 numprocesses = 5 [env:server] INI = private.ini circus-0.12.1/circus/tests/config/issue137.ini000066400000000000000000000003021256046442300210470ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = true [watcher:my_app] cmd = boo uid = me gid = me circus-0.12.1/circus/tests/config/issue210.ini000066400000000000000000000003041256046442300210410ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = true [watcher:my_app] cmd = boo graceful_timeout=30 circus-0.12.1/circus/tests/config/issue310.ini000066400000000000000000000003571256046442300210520ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = true [watcher:web] cmd = foo args = --fd $(circus.sockets.web) [socket:web] host = localhost circus-0.12.1/circus/tests/config/issue395.ini000066400000000000000000000003451256046442300210640ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = true [watcher:web] cmd = foo args = --fd $(circus.sockets.web) graceful_timeout = 88 circus-0.12.1/circus/tests/config/issue442.ini000066400000000000000000000005421256046442300210540ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = $(circus.env.circus_stats_endpoint) statsd = $(circus.env.circus_statsd) [watcher:my_app] cmd = boo uid = $(circus.env.circus_uid) gid = $(circus.env.circus_gid) [env] circus_gid = wheel circus_uid = tarek [env:my_app] circus_gid = root circus-0.12.1/circus/tests/config/issue53.ini000066400000000000000000000002631256046442300207720ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 [watcher:test] cmd = /bin/bash args = -q # invalid option, makes bash exit non-zero warmup_delay = 0 numprocesses = 100 circus-0.12.1/circus/tests/config/issue546.ini000066400000000000000000000002211256046442300210530ustar00rootroot00000000000000[watcher:test] cmd = ../bin/chaussette --fd $(circus.sockets.some-socket) numprocesses = 1 use_sockets = True [socket:some-socket] port = 9090 circus-0.12.1/circus/tests/config/issue567.ini000066400000000000000000000000561256046442300210640ustar00rootroot00000000000000[watcher:watcher1] cmd = $(circus.env.GRAVITY)circus-0.12.1/circus/tests/config/issue594.ini000066400000000000000000000003541256046442300210650ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 statsd = true [watcher:my_app] cmd = boo graceful_timeout = 30 stop_signal = INT stop_children = truecircus-0.12.1/circus/tests/config/issue651.ini000066400000000000000000000000341256046442300210520ustar00rootroot00000000000000[circus] check_delay = 10.5 circus-0.12.1/circus/tests/config/issue665.ini000066400000000000000000000003131256046442300210570ustar00rootroot00000000000000[watcher:web1] cmd = foo args = --fd shell = true [watcher:web2] cmd = foo args = --fd shell = true shell_args = bar baz qux [watcher:web3] cmd = foo args = --fd shell = false shell_args = bar baz qux circus-0.12.1/circus/tests/config/issue680.ini000066400000000000000000000003771256046442300210660ustar00rootroot00000000000000[circus] check_delay = -1 [watcher:test1] cmd = sleep 10 numprocesses = 2 priority = 10 [watcher:test2] cmd = sleep 20 numprocesses = 2 priority = 20 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 priority = 30 circus-0.12.1/circus/tests/config/multiple_wildcard.ini000066400000000000000000000002001256046442300231650ustar00rootroot00000000000000[circus] include = multiple_wildcard/*/*.ini [watcher:server] cmd = sleep 120 numprocesses = 5 [env:server] INI = private.ini circus-0.12.1/circus/tests/config/multiple_wildcard/000077500000000000000000000000001256046442300224745ustar00rootroot00000000000000circus-0.12.1/circus/tests/config/multiple_wildcard/subsubdir1/000077500000000000000000000000001256046442300245575ustar00rootroot00000000000000circus-0.12.1/circus/tests/config/multiple_wildcard/subsubdir1/barbaz.ini000066400000000000000000000001211256046442300265130ustar00rootroot00000000000000[watcher:barbaz] cmd = sleep 120 numprocesses = 5 [env:server] INI = public.ini circus-0.12.1/circus/tests/config/multiple_wildcard/subsubdir2/000077500000000000000000000000001256046442300245605ustar00rootroot00000000000000circus-0.12.1/circus/tests/config/multiple_wildcard/subsubdir2/foobar.ini000066400000000000000000000001161256046442300265270ustar00rootroot00000000000000[circus] check_delay = 555 [watcher:foobar] cmd = sleep 120 numprocesses = 5 circus-0.12.1/circus/tests/config/reload_addplugins.ini000066400000000000000000000006071256046442300231540ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 [plugin:myplugin2] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test2 circus-0.12.1/circus/tests/config/reload_addsockets.ini000066400000000000000000000005351256046442300231460ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [socket:mysocket2] host = localhost port = 8889 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_addwatchers.ini000066400000000000000000000005151256046442300233110ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [watcher:test3] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_base.ini000066400000000000000000000004541256046442300217340ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_changearbiter.ini000066400000000000000000000004541256046442300236200ustar00rootroot00000000000000[circus] check_delay = -2 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_changeplugins.ini000066400000000000000000000004741256046442300236530ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher service = test1 watcher = test1 circus-0.12.1/circus/tests/config/reload_changesockets.ini000066400000000000000000000004541256046442300236430ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8889 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_changewatchers.ini000066400000000000000000000004541256046442300240100ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 150 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_delplugins.ini000066400000000000000000000003231256046442300231630ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 circus-0.12.1/circus/tests/config/reload_delsockets.ini000066400000000000000000000003741256046442300231630ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_delwatchers.ini000066400000000000000000000004131256046442300233220ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_numprocesses.ini000066400000000000000000000005171256046442300235500ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 [watcher:test1] cmd = sleep 120 numprocesses = 2 [watcher:test2] cmd = sleep 120 numprocesses = 2 [socket:mysocket] host = localhost port = 8888 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reload_statsd.ini000066400000000000000000000005221256046442300223200ustar00rootroot00000000000000[circus] check_delay = -1 endpoint = tcp://127.0.0.1:7555 pubsub_endpoint = tcp://127.0.0.1:7556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:test1] cmd = sleep 120 [watcher:test2] cmd = sleep 120 [socket:mysocket] host = localhost port = 8889 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = test1 circus-0.12.1/circus/tests/config/reuseport.ini000066400000000000000000000003761256046442300215270ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [socket:reuseport] host = 127.0.0.1 port = 8888 so_reuseport = True [socket:noreuseport] host = 127.0.0.1 port = 8888 circus-0.12.1/circus/tests/config/test_web.ini000066400000000000000000000003131256046442300213020ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:sleeper] cmd = sleep 120 warmup_delay = 0 numprocesses = 1 circus-0.12.1/circus/tests/config/virtualenv.ini000066400000000000000000000003131256046442300216650ustar00rootroot00000000000000[watcher:test] cmd = ../bin/chaussette --fd $(circus.sockets.some-socket) numprocesses = 1 use_sockets = True virtualenv = /tmp/.virtualenvs/test virtualenv_py_ver=3.3 [socket:some-socket] port = 9090 circus-0.12.1/circus/tests/generic.py000066400000000000000000000020621256046442300175110ustar00rootroot00000000000000import sys sys.path.insert(0, './') def resolve_name(name): ret = None parts = name.split('.') cursor = len(parts) module_name = parts[:cursor] last_exc = None while cursor > 0: try: ret = __import__('.'.join(module_name)) break except ImportError as exc: last_exc = exc if cursor == 0: raise cursor -= 1 module_name = parts[:cursor] for part in parts[1:]: try: ret = getattr(ret, part) except AttributeError: if last_exc is not None: raise last_exc raise ImportError(name) if ret is None: if last_exc is not None: raise last_exc raise ImportError(name) return ret if __name__ == '__main__': callback = resolve_name(sys.argv[1]) try: if len(sys.argv) > 2: test_file = sys.argv[2] sys.exit(callback(test_file)) else: sys.exit(callback()) except: sys.exit(1) circus-0.12.1/circus/tests/support.py000066400000000000000000000375061256046442300176240ustar00rootroot00000000000000from tempfile import mkstemp, mkdtemp import os import signal import sys from time import time, sleep from collections import defaultdict import cProfile import pstats import shutil import functools import multiprocessing import socket try: import sysconfig DEBUG = sysconfig.get_config_var('Py_DEBUG') == 1 except ImportError: # py2.6, we don't really care about that flage here # since no one will run Python --with-pydebug in 2.6 DEBUG = 0 try: from unittest import skip, skipIf, TestCase, TestSuite, findTestCases except ImportError: from unittest2 import skip, skipIf, TestCase, TestSuite # NOQA from unittest2 import findTestCases # NOQA from tornado.testing import AsyncTestCase from zmq.eventloop import ioloop import mock import tornado from circus import get_arbiter from circus.util import DEFAULT_ENDPOINT_DEALER, DEFAULT_ENDPOINT_SUB from circus.util import tornado_sleep, ConflictError from circus.util import IS_WINDOWS from circus.client import AsyncCircusClient, make_message ioloop.install() if 'ASYNC_TEST_TIMEOUT' not in os.environ: os.environ['ASYNC_TEST_TIMEOUT'] = '30' class EasyTestSuite(TestSuite): def __init__(self, name): try: super(EasyTestSuite, self).__init__( findTestCases(sys.modules[name])) except KeyError: pass PYTHON = sys.executable # Script used to sleep for a specified amount of seconds. # Should be used instead of the 'sleep' command for # compatibility SLEEP = PYTHON + " -c 'import time;time.sleep(%d)'" def get_ioloop(): from zmq.eventloop.ioloop import ZMQPoller from zmq.eventloop.ioloop import ZMQError, ETERM from tornado.ioloop import PollIOLoop class DebugPoller(ZMQPoller): def __init__(self): super(DebugPoller, self).__init__() self._fds = [] def register(self, fd, events): if fd not in self._fds: self._fds.append(fd) return self._poller.register(fd, self._map_events(events)) def modify(self, fd, events): if fd not in self._fds: self._fds.append(fd) return self._poller.modify(fd, self._map_events(events)) def unregister(self, fd): if fd in self._fds: self._fds.remove(fd) return self._poller.unregister(fd) def poll(self, timeout): """ #737 - For some reason the poller issues events with unexistant FDs, usually with big ints. We have not found yet the reason of this behavior that happens only during the tests. But by filtering out those events, everything works fine. """ z_events = self._poller.poll(1000*timeout) return [(fd, self._remap_events(evt)) for fd, evt in z_events if fd in self._fds] class DebugLoop(PollIOLoop): def initialize(self, **kwargs): PollIOLoop.initialize(self, impl=DebugPoller(), **kwargs) def handle_callback_exception(self, callback): exc_type, exc_value, tb = sys.exc_info() raise exc_value @staticmethod def instance(): PollIOLoop.configure(DebugLoop) return PollIOLoop.instance() def start(self): try: super(DebugLoop, self).start() except ZMQError as e: if e.errno == ETERM: # quietly return on ETERM pass else: raise e from tornado import ioloop ioloop.IOLoop.configure(DebugLoop) return ioloop.IOLoop.instance() def get_available_port(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.bind(("", 0)) return s.getsockname()[1] finally: s.close() class TestCircus(AsyncTestCase): arbiter_factory = get_arbiter arbiters = [] def setUp(self): super(TestCircus, self).setUp() self.files = [] self.dirs = [] self.tmpfiles = [] self._clients = {} self.plugins = [] @property def cli(self): if self.arbiters == []: # nothing is running raise Exception("nothing is running") endpoint = self.arbiters[-1].endpoint if endpoint in self._clients: return self._clients[endpoint] cli = AsyncCircusClient(endpoint=endpoint) self._clients[endpoint] = cli return cli def _stop_clients(self): for client in self._clients.values(): client.stop() self._clients.clear() def get_new_ioloop(self): return get_ioloop() def tearDown(self): for file in self.files + self.tmpfiles: if os.path.exists(file): os.remove(file) for dir in self.dirs: shutil.rmtree(dir) self._stop_clients() for plugin in self.plugins: plugin.stop() for arbiter in self.arbiters: if arbiter.running: try: arbiter.stop() except ConflictError: pass self.arbiters = [] super(TestCircus, self).tearDown() def make_plugin(self, klass, endpoint=DEFAULT_ENDPOINT_DEALER, sub=DEFAULT_ENDPOINT_SUB, check_delay=1, **config): config['active'] = True plugin = klass(endpoint, sub, check_delay, None, **config) self.plugins.append(plugin) return plugin @tornado.gen.coroutine def start_arbiter(self, cmd='support.run_process', stdout_stream=None, debug=True, **kw): testfile, arbiter = self._create_circus( cmd, stdout_stream=stdout_stream, debug=debug, async=True, **kw) self.test_file = testfile self.arbiter = arbiter self.arbiters.append(arbiter) yield self.arbiter.start() @tornado.gen.coroutine def stop_arbiter(self): for watcher in self.arbiter.iter_watchers(): yield self.arbiter.rm_watcher(watcher.name) yield self.arbiter._emergency_stop() @tornado.gen.coroutine def status(self, cmd, **props): resp = yield self.call(cmd, **props) raise tornado.gen.Return(resp.get('status')) @tornado.gen.coroutine def numwatchers(self, cmd, **props): resp = yield self.call(cmd, waiting=True, **props) raise tornado.gen.Return(resp.get('numprocesses')) @tornado.gen.coroutine def numprocesses(self, cmd, **props): resp = yield self.call(cmd, waiting=True, **props) raise tornado.gen.Return(resp.get('numprocesses')) @tornado.gen.coroutine def pids(self): resp = yield self.call('list', name='test') raise tornado.gen.Return(resp.get('pids')) def get_tmpdir(self): dir_ = mkdtemp() self.dirs.append(dir_) return dir_ def get_tmpfile(self, content=None): fd, file = mkstemp() os.close(fd) self.tmpfiles.append(file) if content is not None: with open(file, 'w') as f: f.write(content) return file @classmethod def _create_circus(cls, callable_path, plugins=None, stats=False, async=False, arbiter_kw=None, **kw): fd, testfile = mkstemp() os.close(fd) wdir = os.path.dirname(os.path.dirname(os.path.dirname( os.path.realpath(__file__)))) args = ['circus/tests/generic.py', callable_path, testfile] worker = {'cmd': PYTHON, 'args': args, 'working_dir': wdir, 'name': 'test', 'graceful_timeout': 2} worker.update(kw) if not arbiter_kw: arbiter_kw = {} debug = arbiter_kw['debug'] = kw.get('debug', arbiter_kw.get('debug', False)) # -1 => no periodic callback to manage_watchers by default arbiter_kw['check_delay'] = kw.get('check_delay', arbiter_kw.get('check_delay', -1)) _gp = get_available_port arbiter_kw['controller'] = "tcp://127.0.0.1:%d" % _gp() arbiter_kw['pubsub_endpoint'] = "tcp://127.0.0.1:%d" % _gp() arbiter_kw['multicast_endpoint'] = "udp://237.219.251.97:12027" if stats: arbiter_kw['statsd'] = True arbiter_kw['stats_endpoint'] = "tcp://127.0.0.1:%d" % _gp() arbiter_kw['statsd_close_outputs'] = not debug if async: arbiter_kw['background'] = False arbiter_kw['loop'] = get_ioloop() else: arbiter_kw['background'] = True arbiter = cls.arbiter_factory([worker], plugins=plugins, **arbiter_kw) cls.arbiters.append(arbiter) return testfile, arbiter def _run_circus(self, callable_path, plugins=None, stats=False, **kw): testfile, arbiter = TestCircus._create_circus(callable_path, plugins, stats, **kw) self.arbiters.append(arbiter) self.files.append(testfile) return testfile @tornado.gen.coroutine def _stop_runners(self): for arbiter in self.arbiters: yield arbiter.stop() self.arbiters = [] @tornado.gen.coroutine def call(self, _cmd, **props): msg = make_message(_cmd, **props) resp = yield self.cli.call(msg) raise tornado.gen.Return(resp) def profile(func): """Can be used to dump profile stats""" def _profile(*args, **kw): profiler = cProfile.Profile() try: return profiler.runcall(func, *args, **kw) finally: pstats.Stats(profiler).sort_stats('time').print_stats(30) return _profile class Process(object): def __init__(self, testfile): self.testfile = testfile # init signal handling if IS_WINDOWS: signal.signal(signal.SIGABRT, self.handle_quit) signal.signal(signal.SIGTERM, self.handle_quit) signal.signal(signal.SIGINT, self.handle_quit) signal.signal(signal.SIGILL, self.handle_quit) signal.signal(signal.SIGBREAK, self.handle_quit) else: signal.signal(signal.SIGQUIT, self.handle_quit) signal.signal(signal.SIGTERM, self.handle_quit) signal.signal(signal.SIGINT, self.handle_quit) signal.signal(signal.SIGCHLD, self.handle_chld) self.alive = True def _write(self, msg): with open(self.testfile, 'a+') as f: f.write(msg) def handle_quit(self, *args): self._write('QUIT') self.alive = False def handle_chld(self, *args): self._write('CHLD') return def run(self): self._write('START') while self.alive: sleep(0.1) self._write('STOP') def run_process(test_file): process = Process(test_file) process.run() return 1 def has_gevent(): try: import gevent # NOQA return True except ImportError: return False def has_circusweb(): try: import circusweb # NOQA return True except ImportError: return False class TimeoutException(Exception): pass def poll_for_callable(func, *args, **kwargs): """Replay to update the status during timeout seconds.""" timeout = 5 if 'timeout' in kwargs: timeout = kwargs.pop('timeout') start = time() last_exception = None while time() - start < timeout: try: func_args = [] for arg in args: if callable(arg): func_args.append(arg()) else: func_args.append(arg) func(*func_args) except AssertionError as e: last_exception = e sleep(0.1) else: return True raise last_exception or AssertionError('No exception triggered yet') def poll_for(filename, needles, timeout=5): """Poll a file for a given string. Raises a TimeoutException if the string isn't found after timeout seconds of polling. """ if isinstance(needles, str): needles = [needles] start = time() needle = content = None while time() - start < timeout: with open(filename) as f: content = f.read() for needle in needles: if needle in content: return True # When using gevent this will make sure the redirector greenlets are # scheduled. sleep(0.1) raise TimeoutException('Timeout polling "%s" for "%s". Content: %s' % ( filename, needle, content)) @tornado.gen.coroutine def async_poll_for(filename, needles, timeout=5): """Async version of poll_for """ if isinstance(needles, str): needles = [needles] start = time() needle = content = None while time() - start < timeout: with open(filename) as f: content = f.read() for needle in needles: if needle in content: raise tornado.gen.Return(True) yield tornado_sleep(0.1) raise TimeoutException('Timeout polling "%s" for "%s". Content: %s' % ( filename, needle, content)) def truncate_file(filename): """Truncate a file (empty it).""" open(filename, 'w').close() # opening as 'w' overwrites the file def run_plugin(klass, config, plugin_info_callback=None, duration=300, endpoint=DEFAULT_ENDPOINT_DEALER, pubsub_endpoint=DEFAULT_ENDPOINT_SUB): check_delay = 1 ssh_server = None class _Statsd(object): gauges = [] increments = defaultdict(int) def gauge(self, name, value): self.gauges.append((name, value)) def increment(self, name): self.increments[name] += 1 def stop(self): pass _statsd = _Statsd() plugin = klass(endpoint, pubsub_endpoint, check_delay, ssh_server, **config) # make sure we close the existing statsd client if hasattr(plugin, 'statsd'): plugin.statsd.stop() plugin.statsd = _statsd deadline = time() + (duration / 1000.) plugin.loop.add_timeout(deadline, plugin.stop) plugin.start() try: if plugin_info_callback: plugin_info_callback(plugin) finally: plugin.stop() return _statsd @tornado.gen.coroutine def async_run_plugin(klass, config, plugin_info_callback, duration=300, endpoint=DEFAULT_ENDPOINT_DEALER, pubsub_endpoint=DEFAULT_ENDPOINT_SUB): queue = multiprocessing.Queue() plugin_info_callback = functools.partial(plugin_info_callback, queue) circusctl_process = multiprocessing.Process( target=run_plugin, args=(klass, config, plugin_info_callback, duration, endpoint, pubsub_endpoint)) circusctl_process.start() while queue.empty(): yield tornado_sleep(.1) result = queue.get() raise tornado.gen.Return(result) class FakeProcess(object): def __init__(self, pid, status, started=1, age=1): self.status = status self.pid = pid self.started = started self.age = age self.stopping = False def is_alive(self): return True def stop(self): pass class MagicMockFuture(mock.MagicMock, tornado.concurrent.Future): def cancel(self): return False def cancelled(self): return False def running(self): return False def done(self): return True def result(self, timeout=None): return None def exception(self, timeout=None): return None def add_done_callback(self, fn): fn(self) def set_result(self, result): pass def set_exception(self, exception): pass def __del__(self): # Don't try to print non-consumed exceptions pass circus-0.12.1/circus/tests/test_arbiter.py000066400000000000000000000612501256046442300205700ustar00rootroot00000000000000import os import signal import socket import tornado from tempfile import mkstemp from time import time import zmq.utils.jsonapi as json import mock try: from urllib.parse import urlparse except ImportError: from urlparse import urlparse # NOQA from circus.arbiter import Arbiter from circus.client import CircusClient from circus.plugins import CircusPlugin from circus.tests.support import (TestCircus, async_poll_for, truncate_file, EasyTestSuite, skipIf, get_ioloop, SLEEP, PYTHON) from circus.util import (DEFAULT_ENDPOINT_DEALER, DEFAULT_ENDPOINT_MULTICAST, DEFAULT_ENDPOINT_SUB) from circus.watcher import Watcher from circus.tests.support import (has_circusweb, poll_for_callable, get_available_port) from circus import watcher as watcher_mod from circus.py3compat import s _GENERIC = os.path.join(os.path.dirname(__file__), 'generic.py') class Plugin(CircusPlugin): name = 'dummy' def __init__(self, *args, **kwargs): super(Plugin, self).__init__(*args, **kwargs) with open(self.config['file'], 'a+') as f: f.write('PLUGIN STARTED') def handle_recv(self, data): topic, msg = data topic_parts = s(topic).split(".") watcher = topic_parts[1] action = topic_parts[2] with open(self.config['file'], 'a+') as f: f.write('%s:%s' % (watcher, action)) class TestTrainer(TestCircus): def setUp(self): super(TestTrainer, self).setUp() self.old = watcher_mod.tornado_sleep def tearDown(self): watcher_mod.tornado_sleep = self.old super(TestTrainer, self).tearDown() @tornado.gen.coroutine def _call(self, _cmd, **props): resp = yield self.call(_cmd, waiting=True, **props) raise tornado.gen.Return(resp) @tornado.testing.gen_test def test_numwatchers(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("numwatchers") self.assertTrue(resp.get("numwatchers") >= 1) yield self.stop_arbiter() @tornado.testing.gen_test def test_numprocesses(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("numprocesses") self.assertTrue(resp.get("numprocesses") >= 1) yield self.stop_arbiter() @tornado.testing.gen_test def test_processes(self): yield self.start_arbiter(graceful_timeout=0) name = "test_processes" resp = yield self._call("add", name=name, cmd=self._get_cmd(), start=True, options=self._get_options()) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("list", name=name) self.assertEqual(len(resp.get('pids')), 1) resp = yield self._call("incr", name=name) self.assertEqual(resp.get('numprocesses'), 2) resp = yield self._call("incr", name=name, nb=2) self.assertEqual(resp.get('numprocesses'), 4) yield self.stop_arbiter() @tornado.testing.gen_test def test_watchers(self): yield self.start_arbiter(graceful_timeout=0) name = "test_watchers" resp = yield self._call("add", name=name, cmd=self._get_cmd(), start=True, options=self._get_options()) resp = yield self._call("list") self.assertTrue(name in resp.get('watchers')) yield self.stop_arbiter() def _get_cmd(self): fd, testfile = mkstemp() os.close(fd) cmd = '%s %s %s %s' % ( PYTHON, _GENERIC, 'circus.tests.support.run_process', testfile) return cmd def _get_cmd_args(self): cmd = PYTHON args = [_GENERIC, 'circus.tests.support.run_process'] return cmd, args def _get_options(self, **kwargs): if 'graceful_timeout' not in kwargs: kwargs['graceful_timeout'] = 4 return kwargs @tornado.testing.gen_test def test_add_watcher(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("add", name="test_add_watcher", cmd=self._get_cmd(), options=self._get_options()) self.assertEqual(resp.get("status"), "ok") yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher_arbiter_stopped(self): yield self.start_arbiter(graceful_timeout=0) # stop the arbiter resp = yield self._call("stop") self.assertEqual(resp.get("status"), "ok") resp = yield self._call("add", name="test_add_watcher_arbiter_stopped", cmd=self._get_cmd(), options=self._get_options()) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("start") self.assertEqual(resp.get("status"), "ok") yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher1(self): yield self.start_arbiter(graceful_timeout=0) name = "test_add_watcher1" yield self._call("add", name=name, cmd=self._get_cmd(), options=self._get_options()) resp = yield self._call("list") self.assertTrue(name in resp.get('watchers')) yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher2(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("numwatchers") before = resp.get("numwatchers") name = "test_add_watcher2" yield self._call("add", name=name, cmd=self._get_cmd(), options=self._get_options()) resp = yield self._call("numwatchers") self.assertEqual(resp.get("numwatchers"), before + 1) yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher_already_exists(self): yield self.start_arbiter(graceful_timeout=0) options = {'name': 'test_add_watcher3', 'cmd': self._get_cmd(), 'options': self._get_options()} yield self._call("add", **options) resp = yield self._call("add", **options) self.assertTrue(resp.get('status'), 'error') self.assertTrue(self.arbiter._exclusive_running_command is None) yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher4(self): yield self.start_arbiter(graceful_timeout=0) cmd, args = self._get_cmd_args() resp = yield self._call("add", name="test_add_watcher4", cmd=cmd, args=args, options=self._get_options()) self.assertEqual(resp.get("status"), "ok") yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher5(self): yield self.start_arbiter(graceful_timeout=0) name = "test_add_watcher5" cmd, args = self._get_cmd_args() resp = yield self._call("add", name=name, cmd=cmd, args=args, options=self._get_options()) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("start", name=name) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("status", name=name) self.assertEqual(resp.get("status"), "active") yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher6(self): yield self.start_arbiter(graceful_timeout=0) name = 'test_add_watcher6' cmd, args = self._get_cmd_args() resp = yield self._call("add", name=name, cmd=cmd, args=args, start=True, options=self._get_options()) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("status", name=name) self.assertEqual(resp.get("status"), "active") yield self.stop_arbiter() @tornado.testing.gen_test def test_add_watcher7(self): yield self.start_arbiter(graceful_timeout=0) cmd, args = self._get_cmd_args() name = 'test_add_watcher7' resp = yield self._call("add", name=name, cmd=cmd, args=args, start=True, options=self._get_options(send_hup=True)) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("status", name=name) self.assertEqual(resp.get("status"), "active") resp = yield self._call("options", name=name) options = resp.get('options', {}) self.assertEqual(options.get("send_hup"), True) yield self.stop_arbiter() @tornado.testing.gen_test def test_rm_watcher(self): yield self.start_arbiter(graceful_timeout=0) name = 'test_rm_watcher' yield self._call("add", name=name, cmd=self._get_cmd(), options=self._get_options()) resp = yield self._call("numwatchers") before = resp.get("numwatchers") yield self._call("rm", name=name) resp = yield self._call("numwatchers") self.assertEqual(resp.get("numwatchers"), before - 1) yield self.stop_arbiter() @tornado.testing.gen_test def test_rm_watcher_nostop(self): # start watcher, save off the pids for the watcher processes we # started, stop the watcher without stopping processes, and validate # the processes are still running, then kill the processes yield self.start_arbiter(graceful_timeout=0) name = 'test_rm_watcher_nostop' yield self._call("add", name=name, cmd=self._get_cmd(), start=True, options=self._get_options()) resp = yield self._call("list", name=name) pids = resp.get('pids') self.assertEqual(len(pids), 1) yield self._call("rm", name=name, nostop=True) try: pid = pids[0] os.kill(pid, 0) os.kill(pid, signal.SIGTERM) os.waitpid(pid, 0) except OSError: self.assertFalse(True, "process was incorrectly killed") yield self.stop_arbiter() @tornado.testing.gen_test def _test_stop(self): resp = yield self._call("quit") self.assertEqual(resp.get("status"), "ok") @tornado.testing.gen_test def test_reload(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("reload") self.assertEqual(resp.get("status"), "ok") yield self.stop_arbiter() @tornado.testing.gen_test def test_reload1(self): yield self.start_arbiter(graceful_timeout=0) name = 'test_reload1' yield self._call("add", name=name, cmd=self._get_cmd(), start=True, options=self._get_options()) resp = yield self._call("list", name=name) processes1 = resp.get('pids') truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("list", name=name) processes2 = resp.get('pids') self.assertNotEqual(processes1, processes2) yield self.stop_arbiter() @tornado.testing.gen_test def test_reload_uppercase(self): yield self.start_arbiter(graceful_timeout=0) name = 'test_RELOAD' yield self._call("add", name=name, cmd=self._get_cmd(), start=True, options=self._get_options()) resp = yield self._call("list", name=name) processes1 = resp.get('pids') truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("list", name=name) processes2 = resp.get('pids') self.assertNotEqual(processes1, processes2) yield self.stop_arbiter() @tornado.testing.gen_test def test_reload_sequential(self): yield self.start_arbiter(graceful_timeout=0) name = 'test_reload_sequential' options = self._get_options(numprocesses=4) yield self._call("add", name=name, cmd=self._get_cmd(), start=True, options=options) resp = yield self._call("list", name=name) processes1 = resp.get('pids') truncate_file(self.test_file) # clean slate yield self._call("reload", sequential=True) self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("list", name=name) processes2 = resp.get('pids') self.assertNotEqual(processes1, processes2) yield self.stop_arbiter() @tornado.testing.gen_test def test_reload2(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("list", name="test") processes1 = resp.get('pids') self.assertEqual(len(processes1), 1) truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("list", name="test") processes2 = resp.get('pids') self.assertEqual(len(processes2), 1) self.assertNotEqual(processes1[0], processes2[0]) yield self.stop_arbiter() @tornado.testing.gen_test def test_reload_wid_1_worker(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("stats", name="test") processes1 = list(resp['info'].keys()) self.assertEqual(len(processes1), 1) wids1 = [resp['info'][process]['wid'] for process in processes1] self.assertEqual(wids1, [1]) truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("stats", name="test") processes2 = list(resp['info'].keys()) self.assertEqual(len(processes2), 1) self.assertNotEqual(processes1, processes2) wids2 = [resp['info'][process]['wid'] for process in processes2] self.assertEqual(wids2, [2]) truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("stats", name="test") processes3 = list(resp['info'].keys()) self.assertEqual(len(processes3), 1) self.assertNotIn(processes3[0], (processes1[0], processes2[0])) wids3 = [resp['info'][process]['wid'] for process in processes3] self.assertEqual(wids3, [1]) yield self.stop_arbiter() @tornado.testing.gen_test def test_reload_wid_4_workers(self): yield self.start_arbiter(graceful_timeout=0) resp = yield self._call("incr", name="test", nb=3) self.assertEqual(resp.get('numprocesses'), 4) resp = yield self._call("stats", name="test") processes1 = list(resp['info'].keys()) self.assertEqual(len(processes1), 4) wids1 = set(resp['info'][process]['wid'] for process in processes1) self.assertSetEqual(wids1, set([1, 2, 3, 4])) truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("stats", name="test") processes2 = list(resp['info'].keys()) self.assertEqual(len(processes2), 4) self.assertEqual(len(set(processes1) & set(processes2)), 0) wids2 = set(resp['info'][process]['wid'] for process in processes2) self.assertSetEqual(wids2, set([5, 6, 7, 8])) truncate_file(self.test_file) # clean slate yield self._call("reload") self.assertTrue(async_poll_for(self.test_file, 'START')) # restarted resp = yield self._call("stats", name="test") processes3 = list(resp['info'].keys()) self.assertEqual(len(processes3), 4) self.assertEqual(len(set(processes1) & set(processes3)), 0) self.assertEqual(len(set(processes2) & set(processes3)), 0) wids3 = set([resp['info'][process]['wid'] for process in processes3]) self.assertSetEqual(wids3, set([1, 2, 3, 4])) yield self.stop_arbiter() @tornado.testing.gen_test def test_stop_watchers(self): yield self.start_arbiter(graceful_timeout=0) yield self._call("stop") resp = yield self._call("status", name="test") self.assertEqual(resp.get("status"), "stopped") yield self._call("start") resp = yield self._call("status", name="test") self.assertEqual(resp.get("status"), 'active') yield self.stop_arbiter() @tornado.testing.gen_test def test_stop_watchers3(self): yield self.start_arbiter(graceful_timeout=0) cmd, args = self._get_cmd_args() name = "test_stop_watchers3" resp = yield self._call("add", name=name, cmd=cmd, args=args, options=self._get_options()) self.assertEqual(resp.get("status"), "ok") resp = yield self._call("start", name=name) self.assertEqual(resp.get("status"), "ok") yield self._call("stop", name=name) resp = yield self._call("status", name=name) self.assertEqual(resp.get('status'), "stopped") yield self._call("start", name=name) resp = yield self._call("status", name=name) self.assertEqual(resp.get('status'), "active") yield self.stop_arbiter() # XXX TODO @tornado.testing.gen_test def _test_plugins(self): fd, datafile = mkstemp() os.close(fd) # setting up a circusd with a plugin dummy_process = 'circus.tests.support.run_process' plugin = 'circus.tests.test_arbiter.Plugin' plugins = [{'use': plugin, 'file': datafile}] self._run_circus(dummy_process, plugins=plugins) # doing a few operations def nb_processes(): return len(cli.send_message('list', name='test').get('pids')) def incr_processes(): return cli.send_message('incr', name='test') # wait for the plugin to be started self.assertTrue(async_poll_for(datafile, 'PLUGIN STARTED')) cli = CircusClient() self.assertEqual(nb_processes(), 1) incr_processes() self.assertEqual(nb_processes(), 2) # wait for the plugin to receive the signal self.assertTrue(async_poll_for(datafile, 'test:spawn')) truncate_file(datafile) incr_processes() self.assertEqual(nb_processes(), 3) # wait for the plugin to receive the signal self.assertTrue(async_poll_for(datafile, 'test:spawn')) # XXX TODO @tornado.testing.gen_test def _test_singleton(self): self._stop_runners() dummy_process = 'circus.tests.support.run_process' self._run_circus(dummy_process, singleton=True) cli = CircusClient() # adding more than one process should fail res = cli.send_message('incr', name='test') self.assertEqual(res['numprocesses'], 1) # TODO XXX @tornado.testing.gen_test def _test_udp_discovery(self): """test_udp_discovery: Test that when the circusd answer UDP call. """ self._stop_runners() dummy_process = 'circus.tests.support.run_process' self._run_circus(dummy_process) ANY = '0.0.0.0' multicast_addr, multicast_port = urlparse(DEFAULT_ENDPOINT_MULTICAST)\ .netloc.split(':') sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.bind((ANY, 0)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.sendto(json.dumps(''), (multicast_addr, int(multicast_port))) timer = time() resp = False endpoints = [] while time() - timer < 10: data, address = sock.recvfrom(1024) data = json.loads(data) endpoint = data.get('endpoint', "") if endpoint == DEFAULT_ENDPOINT_DEALER: resp = True break endpoints.append(endpoint) if not resp: print(endpoints) self.assertTrue(resp) # XXX TODO @tornado.testing.gen_test def _test_start_watchers_warmup_delay(self): yield self.start_arbiter() called = [] @tornado.gen.coroutine def _sleep(duration): called.append(duration) loop = get_ioloop() yield tornado.gen.Task(loop.add_timeout, time() + duration) watcher_mod.tornado_sleep = _sleep watcher = MockWatcher(name='foo', cmd=SLEEP % 1, priority=1) yield self.arbiter.start_watcher(watcher) self.assertTrue(called, [self.arbiter.warmup_delay]) # now make sure we don't sleep when there is a autostart watcher = MockWatcher(name='foo', cmd='serve', priority=1, autostart=False) yield self.arbiter.start_watcher(watcher) self.assertTrue(called, [self.arbiter.warmup_delay]) yield self.stop_arbiter() class MockWatcher(Watcher): def start(self): self.started = True def spawn_process(self): self.processes[1] = 'dummy' class TestArbiter(TestCircus): """ Unit tests for the arbiter class to codify requirements within behavior. """ def test_start_with_callback(self): controller = "tcp://127.0.0.1:%d" % get_available_port() sub = "tcp://127.0.0.1:%d" % get_available_port() arbiter = Arbiter([], controller, sub, check_delay=-1) callee = mock.MagicMock() def callback(*args): callee() arbiter.stop() arbiter.start(cb=callback) self.assertEqual(callee.call_count, 1) @tornado.testing.gen_test def test_start_with_callback_and_given_loop(self): controller = "tcp://127.0.0.1:%d" % get_available_port() sub = "tcp://127.0.0.1:%d" % get_available_port() arbiter = Arbiter([], controller, sub, check_delay=-1, loop=get_ioloop()) callback = mock.MagicMock() try: yield arbiter.start(cb=callback) finally: yield arbiter.stop() self.assertEqual(callback.call_count, 0) @tornado.testing.gen_test def test_start_watcher(self): watcher = MockWatcher(name='foo', cmd='serve', priority=1) arbiter = Arbiter([], None, None, check_delay=-1) yield arbiter.start_watcher(watcher) self.assertTrue(watcher.is_active()) def test_start_watchers_with_autostart(self): watcher = MockWatcher(name='foo', cmd='serve', priority=1, autostart=False) arbiter = Arbiter([], None, None, check_delay=-1) arbiter.start_watcher(watcher) self.assertFalse(getattr(watcher, 'started', False)) @tornado.testing.gen_test def test_add_watcher(self): controller = "tcp://127.0.0.1:%d" % get_available_port() sub = "tcp://127.0.0.1:%d" % get_available_port() arbiter = Arbiter([], controller, sub, loop=get_ioloop(), check_delay=-1) arbiter.add_watcher('foo', SLEEP % 5) try: yield arbiter.start() self.assertEqual(arbiter.watchers[0].status(), 'active') finally: yield arbiter.stop() @tornado.testing.gen_test def test_start_arbiter_with_autostart(self): arbiter = Arbiter([], DEFAULT_ENDPOINT_DEALER, DEFAULT_ENDPOINT_SUB, loop=get_ioloop(), check_delay=-1) arbiter.add_watcher('foo', SLEEP % 5, autostart=False) try: yield arbiter.start() self.assertEqual(arbiter.watchers[0].status(), 'stopped') finally: yield arbiter.stop() @skipIf(not has_circusweb(), 'Tests for circus-web') class TestCircusWeb(TestCircus): @tornado.testing.gen_test def test_circushttpd(self): controller = "tcp://127.0.0.1:%d" % get_available_port() sub = "tcp://127.0.0.1:%d" % get_available_port() arbiter = Arbiter([], controller, sub, loop=get_ioloop(), check_delay=-1, httpd=True, debug=True) self.arbiters.append(arbiter) try: yield arbiter.start() poll_for_callable(self.assertDictEqual, arbiter.statuses, {'circushttpd': 'active'}) finally: yield arbiter.stop() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_circusctl.py000066400000000000000000000140121256046442300211250ustar00rootroot00000000000000import subprocess import shlex from multiprocessing import Process, Queue from tornado.testing import gen_test from tornado.gen import coroutine, Return from circus.circusctl import USAGE, VERSION, CircusCtl from circus.tests.support import (TestCircus, async_poll_for, EasyTestSuite, skipIf, DEBUG, PYTHON, SLEEP) from circus.util import tornado_sleep, DEFAULT_ENDPOINT_DEALER from circus.py3compat import b, s def run_ctl(args, queue=None, stdin='', endpoint=DEFAULT_ENDPOINT_DEALER): cmd = '%s -m circus.circusctl' % PYTHON if '--endpoint' not in args: args = '--endpoint %s ' % endpoint + args proc = subprocess.Popen(cmd.split() + shlex.split(args), stdin=subprocess.PIPE if stdin else None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate(b(stdin) if stdin else None) stdout = s(stdout) stderr = s(stderr) if queue: queue.put(stderr) queue.put(stdout) try: import gevent if hasattr(gevent, 'shutdown'): gevent.shutdown() except ImportError: pass return stdout, stderr @coroutine def async_run_ctl(args, stdin='', endpoint=DEFAULT_ENDPOINT_DEALER): """ Start a process that will start the actual circusctl process and poll its ouput, via a queue, without blocking the I/O loop. We do this to avoid blocking the main thread while waiting for circusctl output, so that the arbiter will be able to respond to requests coming from circusctl. """ queue = Queue() circusctl_process = Process(target=run_ctl, args=(args, queue, stdin, endpoint)) circusctl_process.start() while queue.empty(): yield tornado_sleep(.1) stderr = queue.get() stdout = queue.get() raise Return((stdout, stderr)) class CommandlineTest(TestCircus): @skipIf(DEBUG, 'Py_DEBUG=1') @gen_test def test_help_switch_no_command(self): stdout, stderr = yield async_run_ctl('--help') if stderr: self.assertIn('UserWarning', stderr) output = stdout.splitlines() self.assertEqual(output[0], 'usage: ' + USAGE) self.assertEqual(output[2], 'Controls a Circus daemon') self.assertEqual(output[4], 'Commands:') @gen_test def test_help_invalid_command(self): stdout, stderr = yield async_run_ctl('foo') self.assertEqual(stdout, '') err = stderr.splitlines() while err and 'import' in err[0]: del err[0] self.assertEqual(err[0], 'usage: ' + USAGE) self.assertEqual(err[1], 'circusctl.py: error: unrecognized arguments: foo') @skipIf(DEBUG, 'Py_DEBUG=1') @gen_test def test_help_for_add_command(self): stdout, stderr = yield async_run_ctl('--help add') if stderr: self.assertIn('UserWarning', stderr) self.assertEqual(stdout.splitlines()[0], 'Add a watcher') @skipIf(DEBUG, 'Py_DEBUG=1') @gen_test def test_add(self): yield self.start_arbiter() async_poll_for(self.test_file, 'START') ep = self.arbiter.endpoint stdout, stderr = yield async_run_ctl('add test2 "%s"' % SLEEP % 1, endpoint=ep) if stderr: self.assertIn('UserWarning', stderr) self.assertEqual(stdout.strip(), 'ok') stdout, stderr = yield async_run_ctl('status test2', endpoint=ep) if stderr: self.assertIn('UserWarning', stderr) self.assertEqual(stdout.strip(), 'stopped') yield self.stop_arbiter() @skipIf(DEBUG, 'Py_DEBUG=1') @gen_test def test_add_start(self): yield self.start_arbiter() async_poll_for(self.test_file, 'START') ep = self.arbiter.endpoint stdout, stderr = yield async_run_ctl('add --start test2 "%s"' % SLEEP % 1, endpoint=ep) if stderr: self.assertIn('UserWarning', stderr) self.assertEqual(stdout.strip(), 'ok') stdout, stderr = yield async_run_ctl('status test2', endpoint=ep) if stderr: self.assertIn('UserWarning', stderr) self.assertEqual(stdout.strip(), 'active') yield self.stop_arbiter() class CLITest(TestCircus): @coroutine def run_ctl(self, command='', endpoint=DEFAULT_ENDPOINT_DEALER): """Send the given command to the CLI, and ends with EOF.""" if command: command += '\n' stdout, stderr = yield async_run_ctl('', command + 'EOF\n', endpoint=endpoint) raise Return((stdout, stderr)) @skipIf(DEBUG, 'Py_DEBUG=1') @gen_test def test_launch_cli(self): yield self.start_arbiter() async_poll_for(self.test_file, 'START') stdout, stderr = yield self.run_ctl(endpoint=self.arbiter.endpoint) if stderr: self.assertIn('UserWarning', stderr) output = stdout.splitlines() self.assertEqual(output[0], VERSION) # strip off term escape characters, if any if not output[2].startswith(CircusCtl.prompt): prompt = output[2][-len(CircusCtl.prompt):] self.assertEqual(prompt, CircusCtl.prompt) yield self.stop_arbiter() @gen_test def test_cli_help(self): yield self.start_arbiter() stdout, stderr = yield self.run_ctl('help', endpoint=self.arbiter.endpoint) self.assertEqual(stderr, '') prompt = stdout.splitlines() # first two lines are VERSION and prompt, followed by a blank line self.assertEqual(prompt[3], "Documented commands (type help ):") yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_circusd.py000066400000000000000000000066511256046442300206000ustar00rootroot00000000000000import sys import os import tempfile import six from copy import copy from circus.circusd import get_maxfd, daemonize, main from circus import circusd from circus.arbiter import Arbiter from circus.util import REDIRECT_TO from circus import util from circus.tests.support import (has_gevent, TestCase, skipIf, EasyTestSuite, IS_WINDOWS) CIRCUS_INI = os.path.join(os.path.dirname(__file__), 'config', 'circus.ini') class TestCircusd(TestCase): def setUp(self): self.saved = dict(sys.modules) self.argv = copy(sys.argv) self.starter = Arbiter.start Arbiter.start = lambda x: None self.exit = sys.exit sys.exit = lambda x: None self._files = [] if not IS_WINDOWS: self.fork = os.fork os.fork = self._forking self.setsid = os.setsid os.setsid = lambda: None self.dup2 = os.dup2 os.dup2 = lambda x, y: None self.forked = 0 self.closerange = circusd.closerange circusd.closerange = lambda x, y: None self.open = os.open os.open = self._open self.stop = Arbiter.stop Arbiter.stop = lambda x: None self.config = util.configure_logger circusd.configure_logger = util.configure_logger = self._logger def _logger(self, *args, **kw): pass def _open(self, path, *args, **kw): if path == REDIRECT_TO: return return self.open(path, *args, **kw) def tearDown(self): circusd.configure_logger = util.configure_logger = self.config Arbiter.stop = self.stop sys.argv = self.argv os.open = self.open circusd.closerange = self.closerange sys.modules = self.saved Arbiter.start = self.starter sys.exit = self.exit if not IS_WINDOWS: os.fork = self.fork os.dup2 = self.dup2 os.setsid = self.setsid for file in self._files: if os.path.exists(file): os.remove(file) self.forked = 0 def _forking(self): self.forked += 1 return 0 @skipIf('TRAVIS' in os.environ, 'Travis detected') @skipIf(not has_gevent(), "Only when Gevent is loaded") def test_daemon(self): # if gevent is loaded, we want to prevent # daemonize() to work self.assertRaises(ValueError, daemonize) for module in sys.modules.keys(): if module.startswith('gevent'): del sys.modules[module] import gevent sys.modules['gevent'] = gevent self.assertRaises(ValueError, daemonize) def test_maxfd(self): max = get_maxfd() self.assertTrue(isinstance(max, six.integer_types)) @skipIf(has_gevent(), "Gevent is loaded") @skipIf(IS_WINDOWS, "Daemonizing not supported on Windows") def test_daemonize(self): daemonize() self.assertEqual(self.forked, 2) def _get_file(self): fd, path = tempfile.mkstemp() os.close(fd) self._files.append(path) return path def test_main(self): def _check_pid(cls): self.assertTrue(os.path.exists(pid_file)) Arbiter.start = _check_pid pid_file = self._get_file() sys.argv = ['circusd', CIRCUS_INI, '--pidfile', pid_file] main() self.assertFalse(os.path.exists(pid_file)) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_client.py000066400000000000000000000107321256046442300204150ustar00rootroot00000000000000import os import tempfile from tornado.testing import gen_test from tornado.gen import coroutine, Return from circus.util import tornado_sleep from circus.tests.support import TestCircus, EasyTestSuite, IS_WINDOWS from circus.client import make_message from circus.stream import QueueStream class TestClient(TestCircus): @coroutine def status(self, cmd, **props): resp = yield self.call(cmd, **props) raise Return(resp.get('status')) @coroutine def numprocesses(self, cmd, **props): resp = yield self.call(cmd, waiting=True, **props) raise Return(resp.get('numprocesses')) @coroutine def numwatchers(self, cmd, **props): resp = yield self.call(cmd, **props) raise Return(resp.get('numwatchers')) @coroutine def set(self, name, **opts): resp = yield self.status("set", name=name, waiting=True, options=opts) raise Return(resp) @gen_test def test_client(self): # playing around with the watcher yield self.start_arbiter() msg = make_message("numwatchers") resp = yield self.cli.call(msg) self.assertEqual(resp.get("numwatchers"), 1) self.assertEqual((yield self.numprocesses("numprocesses")), 1) self.assertEqual((yield self.set("test", numprocesses=2)), 'ok') self.assertEqual((yield self.numprocesses("numprocesses")), 2) self.assertEqual((yield self.set("test", numprocesses=1)), 'ok') self.assertEqual((yield self.numprocesses("numprocesses")), 1) self.assertEqual((yield self.numwatchers("numwatchers")), 1) self.assertEqual((yield self.call("list")).get('watchers'), ['test']) self.assertEqual((yield self.numprocesses("incr", name="test")), 2) self.assertEqual((yield self.numprocesses("numprocesses")), 2) self.assertEqual((yield self.numprocesses("incr", name="test", nb=2)), 4) self.assertEqual((yield self.numprocesses("decr", name="test", nb=3)), 1) self.assertEqual((yield self.numprocesses("numprocesses")), 1) if IS_WINDOWS: # On Windows we can't set an env to a process without some keys env = dict(os.environ) else: env = {} env['test'] = 2 self.assertEqual((yield self.set("test", env=env)), 'error') env['test'] = '2' self.assertEqual((yield self.set("test", env=env)), 'ok') resp = yield self.call('get', name='test', keys=['env']) options = resp.get('options', {}) self.assertEqual(options.get('env', {}), env) resp = yield self.call('stats', name='test') self.assertEqual(resp['status'], 'ok') resp = yield self.call('globaloptions', name='test') self.assertEqual(resp['options']['pubsub_endpoint'], self.arbiter.pubsub_endpoint) yield self.stop_arbiter() _, tmp_filename = tempfile.mkstemp(prefix='test_hook') def long_hook(*args, **kw): os.unlink(tmp_filename) class TestWithHook(TestCircus): def run_with_hooks(self, hooks): self.stream = QueueStream() self.errstream = QueueStream() dummy_process = 'circus.tests.support.run_process' return self._create_circus(dummy_process, async=True, stdout_stream={'stream': self.stream}, stderr_stream={'stream': self.errstream}, hooks=hooks) @gen_test def test_message_id(self): hooks = {'before_stop': ('circus.tests.test_client.long_hook', False)} testfile, arbiter = self.run_with_hooks(hooks) yield arbiter.start() try: self.assertTrue(os.path.exists(tmp_filename)) msg = make_message("numwatchers") resp = yield self.cli.call(msg) self.assertEqual(resp.get("numwatchers"), 1) # this should timeout resp = yield self.cli.call(make_message("stop")) self.assertEqual(resp.get('status'), 'ok') while arbiter.watchers[0].status() != 'stopped': yield tornado_sleep(.1) resp = yield self.cli.call(make_message("numwatchers")) self.assertEqual(resp.get("numwatchers"), 1) self.assertFalse(os.path.exists(tmp_filename)) finally: if os.path.exists(tmp_filename): os.unlink(tmp_filename) arbiter.stop() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_command_decrproc.py000066400000000000000000000010111256046442300224240ustar00rootroot00000000000000from circus.tests.test_command_incrproc import FakeArbiter from circus.tests.support import TestCircus, EasyTestSuite from circus.commands.decrproc import DecrProcess class DecrProcTest(TestCircus): def test_decr_proc(self): cmd = DecrProcess() arbiter = FakeArbiter() self.assertTrue(arbiter.watchers[0].nb, 1) props = cmd.message('dummy')['properties'] cmd.execute(arbiter, props) self.assertEqual(arbiter.watchers[0].nb, 0) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_command_incrproc.py000066400000000000000000000030561256046442300224550ustar00rootroot00000000000000from circus.tests.support import TestCircus, EasyTestSuite from circus.commands.incrproc import IncrProc class FakeWatcher(object): name = 'one' singleton = False nb = 1 def info(self, *args): if len(args) == 1 and args[0] == 'meh': raise KeyError('meh') return 'yeah' process_info = info def incr(self, nb): self.nb += nb def decr(self, nb): self.nb -= nb class FakeLoop(object): def add_callback(self, function): function() class FakeArbiter(object): watcher_class = FakeWatcher def __init__(self): self.watchers = [self.watcher_class()] self.loop = FakeLoop() def get_watcher(self, name): return self.watchers[0] def stop_watchers(self, **options): self.watchers[:] = [] def stop(self, **options): self.stop_watchers(**options) class IncrProcTest(TestCircus): def test_incr_proc_message(self): cmd = IncrProc() message = cmd.message('dummy') self.assertTrue(message['properties'], {'name': 'dummy'}) message = cmd.message('dummy', 3) props = sorted(message['properties'].items()) self.assertEqual(props, [('name', 'dummy'), ('nb', 3)]) def test_incr_proc(self): cmd = IncrProc() arbiter = FakeArbiter() size_before = arbiter.watchers[0].nb props = cmd.message('dummy', 3)['properties'] cmd.execute(arbiter, props) self.assertEqual(arbiter.watchers[0].nb, size_before + 3) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_command_list.py000066400000000000000000000011251256046442300216040ustar00rootroot00000000000000from circus.tests.support import TestCircus, EasyTestSuite from circus.commands.list import List class ListCommandTest(TestCircus): def test_list_watchers(self): cmd = List() self.assertTrue( cmd.console_msg({'watchers': ['foo', 'bar']}), 'foo,bar') def test_list_processors(self): cmd = List() self.assertTrue( cmd.console_msg({'pids': [12, 13]}), '12,13') def test_list_error(self): cmd = List() self.assertTrue("error" in cmd.console_msg({'foo': 'bar'})) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_command_quit.py000066400000000000000000000007531256046442300216210ustar00rootroot00000000000000from circus.tests.test_command_incrproc import FakeArbiter from circus.tests.support import TestCircus, EasyTestSuite from circus.commands.quit import Quit class QuitTest(TestCircus): def test_quit(self): cmd = Quit() arbiter = FakeArbiter() self.assertTrue(arbiter.watchers[0].nb, 1) props = cmd.message('dummy')['properties'] cmd.execute(arbiter, props) self.assertEqual(len(arbiter.watchers), 0) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_command_set.py000066400000000000000000000041431256046442300214270ustar00rootroot00000000000000from circus.tests.support import TestCircus, EasyTestSuite from circus.tests.test_command_incrproc import FakeArbiter as _FakeArbiter from circus.commands.set import Set class FakeWatcher(object): def __init__(self): self.actions = [] self.options = {} def set_opt(self, key, val): self.options[key] = val def do_action(self, action): self.actions.append(action) class FakeArbiter(_FakeArbiter): watcher_class = FakeWatcher class SetTest(TestCircus): def test_set_stream(self): arbiter = FakeArbiter() cmd = Set() # setting streams props = cmd.message('dummy', 'stdout_stream.class', 'FileStream') props = props['properties'] cmd.execute(arbiter, props) watcher = arbiter.watchers[0] self.assertEqual(watcher.options, {'stdout_stream.class': 'FileStream'}) self.assertEqual(watcher.actions, [0]) # setting hooks props = cmd.message('dummy', 'hooks.before_start', 'some.hook') props = props['properties'] cmd.execute(arbiter, props) watcher = arbiter.watchers[0] self.assertEqual(watcher.options['hooks.before_start'], 'some.hook') self.assertEqual(watcher.actions, [0, 0]) # we can also set several hooks at once props = cmd.message('dummy', 'hooks', 'before_start:some,after_start:hook') props = props['properties'] cmd.execute(arbiter, props) watcher = arbiter.watchers[0] self.assertEqual(watcher.options['hooks.before_start'], 'some') self.assertEqual(watcher.options['hooks.after_start'], 'hook') def test_set_args(self): arbiter = FakeArbiter() cmd = Set() props = cmd.message('dummy2', 'args', '--arg1 1 --arg2 2') props = props['properties'] cmd.execute(arbiter, props) watcher = arbiter.watchers[0] self.assertEqual(watcher.options['args'], '--arg1 1 --arg2 2') test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_command_signal.py000066400000000000000000000220621256046442300221110ustar00rootroot00000000000000import sys import time import signal import multiprocessing import tornado from circus.tests.support import TestCircus, EasyTestSuite, TimeoutException from circus.tests.support import skipIf, IS_WINDOWS from circus.client import AsyncCircusClient from circus.stream import QueueStream, Empty from circus.util import tornado_sleep from circus.py3compat import s exiting = False channels = {0: [], 1: [], 2: [], 3: []} def run_process(child_id, test_file=None, recursive=False, num_children=3): def send(msg): sys.stdout.write('{0}:{1}\n'.format(child_id, msg)) sys.stdout.flush() names = {} signals = "HUP QUIT INT TERM USR1 USR2".split() exit_signals = set("INT TERM".split()) children = [] if not isinstance(child_id, int): child_id = 0 if child_id == 0 or (recursive and child_id < 2): # create children for top level process # or first two second level processes for i in range(num_children): new_child_id = child_id * 10 + i + 1 p = multiprocessing.Process( target=run_process, args=(new_child_id,), kwargs={'recursive': recursive, 'num_children': num_children}) p.daemon = not (recursive and new_child_id < 2) p.start() children.append(p) def callback(sig, frame=None): global exiting name = names[sig] send(name) if name in exit_signals: exiting = True for signal_name in signals: signum = getattr(signal, "SIG%s" % signal_name) names[signum] = signal_name signal.signal(signum, callback) send('STARTED') while not exiting: signal.pause() send('EXITING') def run_process_recursive(child_id): run_process(child_id, recursive=True, num_children=2) @tornado.gen.coroutine def read_from_stream(stream, desired_channel, timeout=5): start = time.time() accumulator = '' if desired_channel not in channels: channels[desired_channel] = [] while not channels[desired_channel] and time.time() - start < timeout: try: data = stream.get_nowait() data = s(data['data']).split('\n') accumulator += data.pop(0) if data: data.insert(0, accumulator) accumulator = data.pop() for line in data: if len(line) > 1 and line[1] == ':': channel, string = line.partition(':')[::2] channels[int(channel)].append(string) except Empty: yield tornado_sleep(0.1) if channels[desired_channel]: raise tornado.gen.Return(channels[desired_channel].pop(0)) raise TimeoutException('Timeout reading queue') class SignalCommandTest(TestCircus): @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_handler(self): stream = QueueStream() cmd = 'circus.tests.test_command_signal.run_process' stdout_stream = {'stream': stream} stderr_stream = {'stream': stream} yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream, stderr_stream=stderr_stream, stats=True, stop_signal=signal.SIGINT, debug=False) # waiting for data to appear in the queue data = yield read_from_stream(stream, 0) self.assertEqual('STARTED', data) # waiting for children data = yield read_from_stream(stream, 3) self.assertEqual('STARTED', data) data = yield read_from_stream(stream, 2) self.assertEqual('STARTED', data) data = yield read_from_stream(stream, 1) self.assertEqual('STARTED', data) # checking that our system is live and running client = AsyncCircusClient(endpoint=self.arbiter.endpoint) res = yield client.send_message('list') watchers = sorted(res['watchers']) self.assertEqual(['circusd-stats', 'test'], watchers) # send USR1 to parent only res = yield client.send_message('signal', name='test', signum='usr1') self.assertEqual(res['status'], 'ok') res = yield read_from_stream(stream, 0) self.assertEqual(res, 'USR1') # send USR2 to children only res = yield client.send_message('signal', name='test', signum='usr2', children=True) self.assertEqual(res['status'], 'ok') res = yield read_from_stream(stream, 1) self.assertEqual(res, 'USR2') res = yield read_from_stream(stream, 2) self.assertEqual(res, 'USR2') res = yield read_from_stream(stream, 3) self.assertEqual(res, 'USR2') # send HUP to parent and children res = yield client.send_message('signal', name='test', signum='hup', recursive=True) self.assertEqual(res['status'], 'ok') res = yield read_from_stream(stream, 0) self.assertEqual(res, 'HUP') res = yield read_from_stream(stream, 1) self.assertEqual(res, 'HUP') res = yield read_from_stream(stream, 2) self.assertEqual(res, 'HUP') res = yield read_from_stream(stream, 3) self.assertEqual(res, 'HUP') # stop process res = yield client.send_message('stop', name='test') self.assertEqual(res['status'], 'ok') res = yield read_from_stream(stream, 0) self.assertEqual(res, 'INT') res = yield read_from_stream(stream, 0) self.assertEqual(res, 'EXITING') res = yield read_from_stream(stream, 1) self.assertEqual(res, 'TERM') res = yield read_from_stream(stream, 1) self.assertEqual(res, 'EXITING') res = yield read_from_stream(stream, 2) self.assertEqual(res, 'TERM') res = yield read_from_stream(stream, 2) self.assertEqual(res, 'EXITING') res = yield read_from_stream(stream, 3) self.assertEqual(res, 'TERM') res = yield read_from_stream(stream, 3) self.assertEqual(res, 'EXITING') timeout = time.time() + 5 stopped = False while time.time() < timeout: res = yield client.send_message('status', name='test') if res['status'] == 'stopped': stopped = True break self.assertEqual(res['status'], 'stopping') self.assertTrue(stopped) yield self.stop_arbiter() class SignalRecursiveCommandTest(TestCircus): @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_handler(self): stream = QueueStream() cmd = 'circus.tests.test_command_signal.run_process_recursive' stdout_stream = {'stream': stream} stderr_stream = {'stream': stream} yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream, stderr_stream=stderr_stream, stats=True, stop_signal=signal.SIGINT, debug=False) def assert_read(channel, *values): for value in values: data = yield read_from_stream(stream, channel) self.assertEqual(data, value) # waiting for all processes to start for c in (0, 1, 2, 11, 12): assert_read(c, 'STARTED') # checking that our system is live and running client = AsyncCircusClient(endpoint=self.arbiter.endpoint) res = yield client.send_message('list') watchers = sorted(res['watchers']) self.assertEqual(['circusd-stats', 'test'], watchers) # send USR1 to parent only res = yield client.send_message('signal', name='test', signum='usr1') self.assertEqual(res['status'], 'ok') assert_read(0, 'USR1') # send USR2 to children only res = yield client.send_message('signal', name='test', signum='usr2', children=True) self.assertEqual(res['status'], 'ok') for c in (1, 2): assert_read(c, 'USR2') # send HUP to parent and children res = yield client.send_message('signal', name='test', signum='hup', recursive=True) self.assertEqual(res['status'], 'ok') for c in (0, 1, 2, 11, 12): assert_read(c, 'HUP') # stop process res = yield client.send_message('stop', name='test') self.assertEqual(res['status'], 'ok') assert_read(0, 'INT', 'EXITING') for c in (1, 2, 11, 12): assert_read(c, 'TERM', 'EXITING') timeout = time.time() + 5 stopped = False while time.time() < timeout: res = yield client.send_message('status', name='test') if res['status'] == 'stopped': stopped = True break self.assertEqual(res['status'], 'stopping') self.assertTrue(stopped) yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) if __name__ == '__main__': run_process(*sys.argv) circus-0.12.1/circus/tests/test_command_stats.py000066400000000000000000000040511256046442300217700ustar00rootroot00000000000000from circus.tests.support import TestCircus, EasyTestSuite from circus.commands.stats import Stats, MessageError _WANTED = """\ foo: one: 1233 xx tarek false 132 132 13 123 xx 1233 xx tarek false 132 132 13 123 xx 1233 xx tarek false 132 132 13 123 xx""" class FakeWatcher(object): name = 'one' def info(self, *args): if len(args) == 2 and args[0] == 'meh': raise KeyError('meh') return 'yeah' process_info = info class FakeArbiter(object): watchers = [FakeWatcher()] def get_watcher(self, name): return FakeWatcher() class StatsCommandTest(TestCircus): def test_console_msg(self): cmd = Stats() info = {'pid': '1233', 'cmdline': 'xx', 'username': 'tarek', 'nice': 'false', 'mem_info1': '132', 'mem_info2': '132', 'cpu': '13', 'mem': '123', 'ctime': 'xx'} info['children'] = [dict(info), dict(info)] res = cmd.console_msg({'name': 'foo', 'status': 'ok', 'info': {'one': info}}) self.assertEqual(res, _WANTED) def test_execute(self): cmd = Stats() arbiter = FakeArbiter() res = cmd.execute(arbiter, {}) self.assertEqual({'infos': {'one': 'yeah'}}, res) # info about a specific watcher props = {'name': 'one'} res = cmd.execute(arbiter, props) res = sorted(res.items()) wanted = [('info', 'yeah'), ('name', 'one')] self.assertEqual(wanted, res) # info about a specific process props = {'process': '123', 'name': 'one'} res = cmd.execute(arbiter, props) res = sorted(res.items()) wanted = [('info', 'yeah'), ('process', '123')] self.assertEqual(wanted, res) # info that breaks props = {'name': 'meh', 'process': 'meh'} self.assertRaises(MessageError, cmd.execute, arbiter, props) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_config.py000066400000000000000000000343661256046442300204150ustar00rootroot00000000000000import os import signal from mock import patch from circus import logger from circus.arbiter import Arbiter from circus.config import get_config from circus.watcher import Watcher from circus.process import Process from circus.sockets import CircusSocket from circus.tests.support import TestCase, EasyTestSuite, IS_WINDOWS from circus.util import replace_gnu_args from circus.py3compat import PY2 HERE = os.path.join(os.path.dirname(__file__)) CONFIG_DIR = os.path.join(HERE, 'config') _CONF = { 'issue137': os.path.join(CONFIG_DIR, 'issue137.ini'), 'include': os.path.join(CONFIG_DIR, 'include.ini'), 'issue210': os.path.join(CONFIG_DIR, 'issue210.ini'), 'issue310': os.path.join(CONFIG_DIR, 'issue310.ini'), 'issue395': os.path.join(CONFIG_DIR, 'issue395.ini'), 'hooks': os.path.join(CONFIG_DIR, 'hooks.ini'), 'env_var': os.path.join(CONFIG_DIR, 'env_var.ini'), 'env_section': os.path.join(CONFIG_DIR, 'env_section.ini'), 'multiple_wildcard': os.path.join(CONFIG_DIR, 'multiple_wildcard.ini'), 'empty_include': os.path.join(CONFIG_DIR, 'empty_include.ini'), 'circus': os.path.join(CONFIG_DIR, 'circus.ini'), 'nope': os.path.join(CONFIG_DIR, 'nope.ini'), 'unexistant': os.path.join(CONFIG_DIR, 'unexistant.ini'), 'issue442': os.path.join(CONFIG_DIR, 'issue442.ini'), 'expand_vars': os.path.join(CONFIG_DIR, 'expand_vars.ini'), 'issue546': os.path.join(CONFIG_DIR, 'issue546.ini'), 'env_everywhere': os.path.join(CONFIG_DIR, 'env_everywhere.ini'), 'copy_env': os.path.join(CONFIG_DIR, 'copy_env.ini'), 'env_sensecase': os.path.join(CONFIG_DIR, 'env_sensecase.ini'), 'issue567': os.path.join(CONFIG_DIR, 'issue567.ini'), 'issue594': os.path.join(CONFIG_DIR, 'issue594.ini'), 'reuseport': os.path.join(CONFIG_DIR, 'reuseport.ini'), 'issue651': os.path.join(CONFIG_DIR, 'issue651.ini'), 'issue665': os.path.join(CONFIG_DIR, 'issue665.ini'), 'issue680': os.path.join(CONFIG_DIR, 'issue680.ini'), 'virtualenv': os.path.join(CONFIG_DIR, 'virtualenv.ini') } def hook(watcher, hook_name): "Yeah that's me" pass class TestConfig(TestCase): def setUp(self): self.saved = os.environ.copy() def tearDown(self): os.environ = self.saved def test_issue310(self): ''' https://github.com/circus-tent/circus/pull/310 Allow $(circus.sockets.name) to be used in args. ''' conf = get_config(_CONF['issue310']) watcher = Watcher.load_from_config(conf['watchers'][0]) socket = CircusSocket.load_from_config(conf['sockets'][0]) try: watcher.initialize(None, {'web': socket}, None) if IS_WINDOWS: # We can't close the sockets on Windows as we # are redirecting stdout watcher.use_sockets = True process = Process('test', watcher._nextwid, watcher.cmd, args=watcher.args, working_dir=watcher.working_dir, shell=watcher.shell, uid=watcher.uid, gid=watcher.gid, env=watcher.env, rlimits=watcher.rlimits, spawn=False, executable=watcher.executable, use_fds=watcher.use_sockets, watcher=watcher) sockets_fds = watcher._get_sockets_fds() formatted_args = process.format_args(sockets_fds=sockets_fds) fd = sockets_fds['web'] self.assertEqual(formatted_args, ['foo', '--fd', str(fd)]) finally: socket.close() def test_issue137(self): conf = get_config(_CONF['issue137']) watcher = conf['watchers'][0] self.assertEqual(watcher['uid'], 'me') def test_issues665(self): ''' https://github.com/circus-tent/circus/pull/665 Ensure args formatting when shell = True. ''' conf = get_config(_CONF['issue665']) def load(watcher_conf): watcher = Watcher.load_from_config(watcher_conf.copy()) # Make sure we don't close the sockets as we will be # launching the Watcher with IS_WINDOWS=True watcher.use_sockets = True process = Process('test', watcher._nextwid, watcher.cmd, args=watcher.args, working_dir=watcher.working_dir, shell=watcher.shell, uid=watcher.uid, gid=watcher.gid, env=watcher.env, rlimits=watcher.rlimits, spawn=False, executable=watcher.executable, use_fds=watcher.use_sockets, watcher=watcher) return process.format_args() import circus.process try: # force nix circus.process.IS_WINDOWS = False # without shell_args with patch.object(logger, 'warn') as mock_logger_warn: formatted_args = load(conf['watchers'][0]) self.assertEqual(formatted_args, ['foo --fd']) self.assertFalse(mock_logger_warn.called) # with shell_args with patch.object(logger, 'warn') as mock_logger_warn: formatted_args = load(conf['watchers'][1]) self.assertEqual(formatted_args, ['foo --fd', 'bar', 'baz', 'qux']) self.assertFalse(mock_logger_warn.called) # with shell_args but not shell with patch.object(logger, 'warn') as mock_logger_warn: formatted_args = load(conf['watchers'][2]) self.assertEqual(formatted_args, ['foo', '--fd']) self.assertTrue(mock_logger_warn.called) # force win circus.process.IS_WINDOWS = True # without shell_args with patch.object(logger, 'warn') as mock_logger_warn: formatted_args = load(conf['watchers'][0]) self.assertEqual(formatted_args, ['foo --fd']) self.assertFalse(mock_logger_warn.called) # with shell_args with patch.object(logger, 'warn') as mock_logger_warn: formatted_args = load(conf['watchers'][1]) self.assertEqual(formatted_args, ['foo --fd']) self.assertTrue(mock_logger_warn.called) # with shell_args but not shell with patch.object(logger, 'warn') as mock_logger_warn: formatted_args = load(conf['watchers'][2]) self.assertEqual(formatted_args, ['foo', '--fd']) self.assertTrue(mock_logger_warn.called) finally: circus.process.IS_WINDOWS = IS_WINDOWS def test_include_wildcards(self): conf = get_config(_CONF['include']) watchers = conf['watchers'] self.assertEqual(len(watchers), 4) def test_include_multiple_wildcards(self): conf = get_config(_CONF['multiple_wildcard']) watchers = conf['watchers'] self.assertEqual(len(watchers), 3) @patch.object(logger, 'warn') def test_empty_include(self, mock_logger_warn): """https://github.com/circus-tent/circus/pull/473""" try: get_config(_CONF['empty_include']) except: self.fail('Non-existent includes should not raise') self.assertTrue(mock_logger_warn.called) def test_watcher_graceful_timeout(self): conf = get_config(_CONF['issue210']) watcher = Watcher.load_from_config(conf['watchers'][0]) watcher.stop() def test_plugin_priority(self): arbiter = Arbiter.load_from_config(_CONF['issue680']) watchers = arbiter.iter_watchers() self.assertEqual(watchers[0].priority, 30) self.assertEqual(watchers[0].name, 'plugin:myplugin') self.assertEqual(watchers[1].priority, 20) self.assertEqual(watchers[1].cmd, 'sleep 20') self.assertEqual(watchers[2].priority, 10) self.assertEqual(watchers[2].cmd, 'sleep 10') def test_hooks(self): conf = get_config(_CONF['hooks']) watcher = Watcher.load_from_config(conf['watchers'][0]) self.assertEqual(watcher.hooks['before_start'].__doc__, hook.__doc__) self.assertTrue('before_start' not in watcher.ignore_hook_failure) def test_watcher_env_var(self): conf = get_config(_CONF['env_var']) watcher = Watcher.load_from_config(conf['watchers'][0]) self.assertEqual("%s:/bin" % os.getenv('PATH'), watcher.env['PATH']) watcher.stop() def test_env_section(self): conf = get_config(_CONF['env_section']) watchers_conf = {} for watcher_conf in conf['watchers']: watchers_conf[watcher_conf['name']] = watcher_conf watcher1 = Watcher.load_from_config(watchers_conf['watcher1']) watcher2 = Watcher.load_from_config(watchers_conf['watcher2']) self.assertEqual('lie', watcher1.env['CAKE']) self.assertEqual('cake', watcher2.env['LIE']) for watcher in [watcher1, watcher2]: self.assertEqual("%s:/bin" % os.getenv('PATH'), watcher.env['PATH']) self.assertEqual('test1', watcher1.env['TEST1']) self.assertEqual('test1', watcher2.env['TEST1']) self.assertEqual('test2', watcher1.env['TEST2']) self.assertEqual('test2', watcher2.env['TEST2']) self.assertEqual('test3', watcher1.env['TEST3']) self.assertEqual('test3', watcher2.env['TEST3']) def test_issue395(self): conf = get_config(_CONF['issue395']) watcher = conf['watchers'][0] self.assertEqual(watcher['graceful_timeout'], 88) def test_pidfile(self): conf = get_config(_CONF['circus']) self.assertEqual(conf['pidfile'], 'pidfile') def test_logoutput(self): conf = get_config(_CONF['circus']) self.assertEqual(conf['logoutput'], 'logoutput') def test_loglevel(self): conf = get_config(_CONF['circus']) self.assertEqual(conf['loglevel'], 'debug') def test_override(self): conf = get_config(_CONF['multiple_wildcard']) watchers = conf['watchers'] self.assertEqual(len(watchers), 3) watchers = conf['watchers'] if PY2: watchers.sort() else: watchers = sorted(watchers, key=lambda a: a['__name__']) self.assertEqual(watchers[2]['env']['INI'], 'private.ini') self.assertEqual(conf['check_delay'], 555) def test_config_unexistant(self): self.assertRaises(IOError, get_config, _CONF['unexistant']) def test_variables_everywhere(self): os.environ['circus_stats_endpoint'] = 'tcp://0.0.0.0:9876' os.environ['circus_statsd'] = 'True' # these will be overriden os.environ['circus_uid'] = 'ubuntu' os.environ['circus_gid'] = 'ubuntu' conf = get_config(_CONF['issue442']) self.assertEqual(conf['stats_endpoint'], 'tcp://0.0.0.0:9876') self.assertTrue(conf['statsd']) self.assertEqual(conf['watchers'][0]['uid'], 'tarek') self.assertEqual(conf['watchers'][0]['gid'], 'root') def test_expand_vars(self): ''' https://github.com/circus-tent/circus/pull/554 ''' conf = get_config(_CONF['expand_vars']) watcher = conf['watchers'][0] self.assertEqual(watcher['stdout_stream']['filename'], '/tmp/echo.log') def test_dashes(self): conf = get_config(_CONF['issue546']) replaced = replace_gnu_args(conf['watchers'][0]['cmd'], sockets={'some-socket': 3}) self.assertEqual(replaced, '../bin/chaussette --fd 3') def test_env_everywhere(self): conf = get_config(_CONF['env_everywhere']) self.assertEqual(conf['endpoint'], 'tcp://127.0.0.1:1234') self.assertEqual(conf['sockets'][0]['path'], '/var/run/broken.sock') self.assertEqual(conf['plugins'][0]['use'], 'bad.has.been.broken') def test_copy_env(self): # #564 make sure we respect copy_env os.environ['BAM'] = '1' conf = get_config(_CONF['copy_env']) for watcher in conf['watchers']: if watcher['name'] == 'watcher1': self.assertFalse('BAM' in watcher['env']) else: self.assertTrue('BAM' in watcher['env']) self.assertTrue('TEST1' in watcher['env']) def test_env_casesense(self): # #730 make sure respect case conf = get_config(_CONF['env_sensecase']) w = conf['watchers'][0] self.assertEqual(w['name'], 'webapp') self.assertTrue('http_proxy' in w['env']) self.assertEqual(w['env']['http_proxy'], 'http://localhost:8080') self.assertTrue('HTTPS_PROXY' in w['env']) self.assertEqual(w['env']['HTTPS_PROXY'], 'http://localhost:8043') self.assertTrue('FunKy_soUl' in w['env']) self.assertEqual(w['env']['FunKy_soUl'], 'scorpio') def test_issue567(self): os.environ['GRAVITY'] = 'down' conf = get_config(_CONF['issue567']) # make sure the global environment makes it into the cfg environment # even without [env] section self.assertEqual(conf['watchers'][0]['cmd'], 'down') def test_watcher_stop_signal(self): conf = get_config(_CONF['issue594']) self.assertEqual(conf['watchers'][0]['stop_signal'], signal.SIGINT) watcher = Watcher.load_from_config(conf['watchers'][0]) watcher.stop() def test_socket_so_reuseport_yes(self): conf = get_config(_CONF['reuseport']) s1 = conf['sockets'][1] self.assertEqual(s1['so_reuseport'], True) def test_socket_so_reuseport_no(self): conf = get_config(_CONF['reuseport']) s1 = conf['sockets'][0] self.assertEqual(s1['so_reuseport'], False) def test_check_delay(self): conf = get_config(_CONF['issue651']) self.assertEqual(conf['check_delay'], 10.5) def test_virtualenv(self): conf = get_config(_CONF['virtualenv']) watcher = conf['watchers'][0] self.assertEqual(watcher['virtualenv'], "/tmp/.virtualenvs/test") self.assertEqual(watcher['virtualenv_py_ver'], "3.3") test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_controller.py000066400000000000000000000045201256046442300213200ustar00rootroot00000000000000from circus.tests.support import TestCase, EasyTestSuite, get_ioloop from circus.controller import Controller from circus.util import DEFAULT_ENDPOINT_MULTICAST from circus import logger import circus.controller import mock class TestController(TestCase): def test_add_job(self): arbiter = mock.MagicMock() class MockedController(Controller): called = False def _init_stream(self): pass # NO OP def initialize(self): pass # NO OP def dispatch(self, job): self.called = True self.loop.stop() loop = get_ioloop() controller = MockedController('endpoint', 'multicast_endpoint', mock.sentinel.context, loop, arbiter, check_delay=-1.0) controller.dispatch((None, 'something')) controller.start() loop.start() self.assertTrue(controller.called) def _multicast_side_effect_helper(self, side_effect): arbiter = mock.MagicMock() loop = mock.MagicMock() context = mock.sentinel.context controller = circus.controller.Controller( 'endpoint', DEFAULT_ENDPOINT_MULTICAST, context, loop, arbiter ) with mock.patch('circus.util.create_udp_socket') as m: m.side_effect = side_effect circus.controller.create_udp_socket = m with mock.patch.object(logger, 'warning') as mock_logger_warn: controller._init_multicast_endpoint() self.assertTrue(mock_logger_warn.called) def test_multicast_ioerror(self): self._multicast_side_effect_helper(IOError) def test_multicast_oserror(self): self._multicast_side_effect_helper(OSError) def test_multicast_valueerror(self): arbiter = mock.MagicMock() loop = mock.MagicMock() context = mock.sentinel.context wrong_multicast_endpoint = 'udp://127.0.0.1:12027' controller = Controller('endpoint', wrong_multicast_endpoint, context, loop, arbiter) with mock.patch.object(logger, 'warning') as mock_logger_warn: controller._init_multicast_endpoint() self.assertTrue(mock_logger_warn.called) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_convert_option.py000066400000000000000000000035671256046442300222170ustar00rootroot00000000000000from circus.tests.support import TestCase, EasyTestSuite from circus.commands.util import convert_option, ArgumentError class TestConvertOption(TestCase): def test_env(self): env = convert_option("env", {"port": "8080"}) self.assertDictEqual({"port": "8080"}, env) def test_stdout_and_stderr_stream(self): expected_convertions = ( ('stdout_stream.class', 'class', 'class'), ('stdout_stream.filename', 'file', 'file'), ('stdout_stream.other_option', 'other', 'other'), ('stdout_stream.refresh_time', '10', '10'), ('stdout_stream.max_bytes', '10', 10), ('stdout_stream.backup_count', '20', 20), ('stderr_stream.class', 'class', 'class'), ('stderr_stream.filename', 'file', 'file'), ('stderr_stream.other_option', 'other', 'other'), ('stderr_stream.refresh_time', '10', '10'), ('stderr_stream.max_bytes', '10', 10), ('stderr_stream.backup_count', '20', 20), ('stderr_stream.some_number', '99', '99'), ('stderr_stream.some_number_2', 99, 99), ) for option, value, expected in expected_convertions: ret = convert_option(option, value) self.assertEqual(ret, expected) def test_hooks(self): ret = convert_option('hooks', 'before_start:one') self.assertEqual(ret, {'before_start': 'one'}) ret = convert_option('hooks', 'before_start:one,after_start:two') self.assertEqual(ret['before_start'], 'one') self.assertEqual(ret['after_start'], 'two') self.assertRaises(ArgumentError, convert_option, 'hooks', 'before_start:one,DONTEXIST:two') self.assertRaises(ArgumentError, convert_option, 'hooks', 'before_start:one:two') test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_dsa000066400000000000000000000012341256046442300172540ustar00rootroot00000000000000-----BEGIN DSA PRIVATE KEY----- MIIBugIBAAKBgQDQq2dQKh03rW54rRzX1F3EyM8f1tEmA2nzIPT4GgJ3MJJqdGs1 2POLf9GWjHcVw3Zm3ismgrqZzNLdTCm/v2+bex8S9u5CWq9idDhnEtXmD1Z73Zuy j73CHljWAsSWlXcS6MFp5UlbYRVW8hH3Go2mX0UhLxBiCsWRwT3sGy7QfwIVAPa4 KJebKkhDp89PwoiydIxtGZalAoGAOX+5i4oVI65eb+qWXvWVUxSOZ4z6SvnNxodN YhVDFhlOlMGqVj1EBjZhpzhluTDRXwjT7jnSOojv4Y0sKVMXtTktkpXQsNldaMDq Jjgw8NpNqs1QyLtUSmasS1fopG6JE7oE5X3hWINzfEnLo44ySUVIodW8LwsQYgSX /M+EAYACgYB55PiceKm6kx4mr/TeYIkVJdUpdnuRLX3KXYCxLDSZHvfndrZYGoJ/ xIC5xSgm0TBVe7Lzd8xbYLnQr25xtLxJZIXEd7AdgnqX/HCL63UGAAf2mzbNLTmw wF2fXOLQKNbuM7wF2fnb/PHU9TtgZ48Y65M9X4Q/qEUi5mVBlni9ggIUSpjsGRl9 acODwvnb4zHc/BNK4PY= -----END DSA PRIVATE KEY----- circus-0.12.1/circus/tests/test_dsa.pub000066400000000000000000000011421256046442300200370ustar00rootroot00000000000000ssh-dss AAAAB3NzaC1kc3MAAACBANCrZ1AqHTetbnitHNfUXcTIzx/W0SYDafMg9PgaAncwkmp0azXY84t/0ZaMdxXDdmbeKyaCupnM0t1MKb+/b5t7HxL27kJar2J0OGcS1eYPVnvdm7KPvcIeWNYCxJaVdxLowWnlSVthFVbyEfcajaZfRSEvEGIKxZHBPewbLtB/AAAAFQD2uCiXmypIQ6fPT8KIsnSMbRmWpQAAAIA5f7mLihUjrl5v6pZe9ZVTFI5njPpK+c3Gh01iFUMWGU6UwapWPUQGNmGnOGW5MNFfCNPuOdI6iO/hjSwpUxe1OS2SldCw2V1owOomODDw2k2qzVDIu1RKZqxLV+ikbokTugTlfeFYg3N8ScujjjJJRUih1bwvCxBiBJf8z4QBgAAAAIB55PiceKm6kx4mr/TeYIkVJdUpdnuRLX3KXYCxLDSZHvfndrZYGoJ/xIC5xSgm0TBVe7Lzd8xbYLnQr25xtLxJZIXEd7AdgnqX/HCL63UGAAf2mzbNLTmwwF2fXOLQKNbuM7wF2fnb/PHU9TtgZ48Y65M9X4Q/qEUi5mVBlni9gg== nick@nick-VirtualBox circus-0.12.1/circus/tests/test_logging.py000066400000000000000000000246031256046442300205670ustar00rootroot00000000000000try: from io import StringIO from io import BytesIO except ImportError: from cStringIO import StringIO # NOQA try: from configparser import ConfigParser except ImportError: from ConfigParser import ConfigParser # NOQA from circus.tests.support import TestCase from circus.tests.support import EasyTestSuite from circus.tests.support import skipIf, PYTHON, IS_WINDOWS import os import shutil import tempfile from pipes import quote as shell_escape_arg import subprocess import time import yaml import json import logging.config import sys HERE = os.path.abspath(os.path.dirname(__file__)) CONFIG_PATH = os.path.join(HERE, 'config', 'circus.ini') def run_circusd(options=(), config=(), log_capture_path="log.txt", additional_files=()): options = list(options) additional_files = dict(additional_files) config_ini_update = { "watcher:touch.cmd": PYTHON, "watcher:touch.args": "-c \"open('workerstart.txt', 'w+').close()\"", "watcher:touch.respawn": 'False' } config_ini_update.update(dict(config)) config_ini = ConfigParser() config_ini.read(CONFIG_PATH) for dottedkey in config_ini_update: section, key = dottedkey.split(".", 1) if section not in config_ini.sections(): config_ini.add_section(section) config_ini.set( section, key, config_ini_update[dottedkey]) temp_dir = tempfile.mkdtemp() try: circus_ini_path = os.path.join(temp_dir, "circus.ini") with open(circus_ini_path, "w") as fh: config_ini.write(fh) for relpath in additional_files: path = os.path.join(temp_dir, relpath) with open(path, "w") as fh: fh.write(additional_files[relpath]) env = os.environ.copy() sep = ';' if IS_WINDOWS else ':' # We're going to run circus from a process with a different # cwd, so we need to make sure that Python will import the # current version of circus pythonpath = env.get('PYTHONPATH', '') pythonpath += '%s%s' % (sep, os.path.abspath( os.path.join(HERE, os.pardir, os.pardir))) env['PYTHONPATH'] = pythonpath argv = ["circus.circusd"] + options + [circus_ini_path] if sys.gettrace() is None or IS_WINDOWS: # killing a coverage run process leaves a zombie on # Windows so we should skip coverage argv = [PYTHON, "-m"] + argv else: exe_dir = os.path.dirname(PYTHON) coverage = os.path.join(exe_dir, "coverage") if not os.path.isfile(coverage): coverage = "coverage" argv = [coverage, "run", "-p", "-m"] + argv child = subprocess.Popen(argv, cwd=temp_dir, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) try: touch_path = os.path.join(temp_dir, "workerstart.txt") while True: child.poll() if os.path.exists(touch_path): break if child.returncode is not None: break time.sleep(0.01) finally: child.terminate() child.wait() log_file_path = os.path.join(temp_dir, log_capture_path) try: if os.path.exists(log_file_path): with open(log_file_path, "r") as fh: return fh.read() else: if child.stdout is not None: raise Exception(child.stdout.read().decode("ascii")) finally: if child.stdout is not None: child.stdout.close() if child.stderr is not None: child.stderr.close() if child.stdin is not None: child.stdin.close() assert child.returncode == 0, \ " ".join(shell_escape_arg(a) for a in argv) finally: for basename in sorted(os.listdir(temp_dir)): if basename.startswith(".coverage."): source = os.path.join(temp_dir, basename) target = os.path.abspath(basename) shutil.copy(source, target) try: shutil.rmtree(temp_dir) except OSError: # Sometimes on Windows we can't delete the # logging file because it says it's still in # use (lock). pass EXAMPLE_YAML = """\ version: 1 disable_existing_loggers: false formatters: simple: format: '%(asctime)s - %(name)s - [%(levelname)s] %(message)s' handlers: logfile: class: logging.FileHandler filename: logoutput.txt level: DEBUG formatter: simple loggers: circus: level: DEBUG handlers: [logfile] propagate: no root: level: DEBUG handlers: [logfile] """ EXPECTED_LOG_MESSAGE = "[INFO] Arbiter now waiting for commands" def logging_dictconfig_to_ini(config): assert config.get("version", 1) == 1, config ini = ConfigParser() ini.add_section("loggers") loggers = config.get("loggers", {}) if "root" in config: loggers["root"] = config["root"] ini.set("loggers", "keys", ",".join(sorted(loggers.keys()))) for logger in sorted(loggers.keys()): section = "logger_%s" % (logger.replace(".", "_"),) ini.add_section(section) for key, value in sorted(loggers[logger].items()): if key == "handlers": value = ",".join(value) if key == "propagate": value = "1" if value else "0" ini.set(section, key, value) ini.set(section, "qualname", logger) ini.add_section("handlers") handlers = config.get("handlers", {}) ini.set("handlers", "keys", ",".join(sorted(handlers.keys()))) for handler in sorted(handlers.keys()): section = "handler_%s" % (handler,) ini.add_section(section) args = [] for key, value in sorted(handlers[handler].items()): if (handlers[handler]["class"] == "logging.FileHandler" and key == "filename"): args.append(value) else: ini.set(section, key, value) ini.set(section, "args", repr(tuple(args))) ini.add_section("formatters") formatters = config.get("formatters", {}) ini.set("formatters", "keys", ",".join(sorted(formatters.keys()))) for formatter in sorted(formatters.keys()): section = "formatter_%s" % (formatter,) ini.add_section(section) for key, value in sorted(formatters[formatter].items()): ini.set(section, key, value) try: # Older Python (without io.StringIO/io.BytesIO) and Python 3 use # this code path. result = StringIO() ini.write(result) return result.getvalue() except TypeError: # Python 2.7 has io.StringIO and io.BytesIO but ConfigParser.write # has not been fixed to work with StringIO. result = BytesIO() ini.write(result) return result.getvalue().decode("ascii") def hasDictConfig(): return hasattr(logging.config, "dictConfig") class TestLoggingConfig(TestCase): def test_loggerconfig_default_ini(self): logs = run_circusd( [], {"circus.logoutput": "log_ini.txt"}, log_capture_path="log_ini.txt") self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) def test_loggerconfig_default_opt(self): logs = run_circusd( ["--log-output", "log_opt.txt"], {}, log_capture_path="log_opt.txt") self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) @skipIf(not hasDictConfig(), "Needs logging.config.dictConfig()") def test_loggerconfig_yaml_ini(self): config = yaml.load(EXAMPLE_YAML) config["handlers"]["logfile"]["filename"] = "log_yaml_ini.txt" logs = run_circusd( [], {"circus.loggerconfig": "logging.yaml"}, log_capture_path="log_yaml_ini.txt", additional_files={"logging.yaml": yaml.dump(config)}) self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) @skipIf(not hasDictConfig(), "Needs logging.config.dictConfig()") def test_loggerconfig_yaml_opt(self): config = yaml.load(EXAMPLE_YAML) config["handlers"]["logfile"]["filename"] = "log_yaml_opt.txt" logs = run_circusd( ["--logger-config", "logging.yaml"], {}, log_capture_path="log_yaml_opt.txt", additional_files={"logging.yaml": yaml.dump(config)}) self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) @skipIf(not hasDictConfig(), "Needs logging.config.dictConfig()") def test_loggerconfig_json_ini(self): config = yaml.load(EXAMPLE_YAML) config["handlers"]["logfile"]["filename"] = "log_json_ini.txt" logs = run_circusd( [], {"circus.loggerconfig": "logging.json"}, log_capture_path="log_json_ini.txt", additional_files={"logging.json": json.dumps(config)}) self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) @skipIf(not hasDictConfig(), "Needs logging.config.dictConfig()") def test_loggerconfig_json_opt(self): config = yaml.load(EXAMPLE_YAML) config["handlers"]["logfile"]["filename"] = "log_json_opt.txt" logs = run_circusd( ["--logger-config", "logging.json"], {}, log_capture_path="log_json_opt.txt", additional_files={"logging.json": json.dumps(config)}) self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) def test_loggerconfig_ini_ini(self): config = yaml.load(EXAMPLE_YAML) config["handlers"]["logfile"]["filename"] = "log_ini_ini.txt" logs = run_circusd( [], {"circus.loggerconfig": "logging.ini"}, log_capture_path="log_ini_ini.txt", additional_files={ "logging.ini": logging_dictconfig_to_ini(config)}) self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) def test_loggerconfig_ini_opt(self): config = yaml.load(EXAMPLE_YAML) config["handlers"]["logfile"]["filename"] = "log_ini_opt.txt" logs = run_circusd( ["--logger-config", "logging.ini"], {}, log_capture_path="log_ini_opt.txt", additional_files={ "logging.ini": logging_dictconfig_to_ini(config)}) self.assertTrue(EXPECTED_LOG_MESSAGE in logs, logs) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_papa_stream.py000066400000000000000000000113461256046442300214350ustar00rootroot00000000000000import time import sys import os import tempfile import tornado import random from circus.util import papa from circus.client import make_message from circus.tests.support import TestCircus, async_poll_for, truncate_file from circus.tests.support import EasyTestSuite, skipIf, IS_WINDOWS from circus.stream import FileStream, WatchedFileStream from circus.stream import TimedRotatingFileStream def run_process(testfile, *args, **kw): try: # print once, then wait sys.stdout.write('stdout') sys.stdout.flush() sys.stderr.write('stderr') sys.stderr.flush() with open(testfile, 'a+') as f: f.write('START') time.sleep(1.) except: return 1 @skipIf(papa is None, "papa not available") @skipIf('TRAVIS' in os.environ, "Skipped on Travis") class TestPapaStream(TestCircus): dummy_process = 'circus.tests.test_stream.run_process' papa_port = random.randint(20000, 30000) def setUp(self): super(TestPapaStream, self).setUp() papa.set_debug_mode(quit_when_connection_closed=True) papa.set_default_port(self.papa_port) papa.set_default_connection_timeout(120) fd, self.stdout = tempfile.mkstemp() os.close(fd) fd, self.stderr = tempfile.mkstemp() os.close(fd) self.stdout_stream = FileStream(self.stdout) self.stderr_stream = FileStream(self.stderr) self.stdout_arg = {'stream': self.stdout_stream} self.stderr_arg = {'stream': self.stderr_stream} def tearDown(self): self.stdout_stream.close() self.stderr_stream.close() if os.path.exists(self.stdout): os.remove(self.stdout) if os.path.exists(self.stderr): os.remove(self.stderr) @tornado.gen.coroutine def _start_arbiter(self): yield self.start_arbiter(cmd=self.dummy_process, stdout_stream=self.stdout_arg, stderr_stream=self.stderr_arg, use_papa=True) @tornado.gen.coroutine def restart_arbiter(self): yield self.arbiter.restart() @tornado.gen.coroutine def call(self, _cmd, **props): msg = make_message(_cmd, **props) resp = yield self.cli.call(msg) raise tornado.gen.Return(resp) @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_file_stream(self): yield self._start_arbiter() stream = FileStream(self.stdout, max_bytes='12', backup_count='3') self.assertTrue(isinstance(stream._max_bytes, int)) self.assertTrue(isinstance(stream._backup_count, int)) yield self.stop_arbiter() stream.close() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_watched_file_stream(self): yield self._start_arbiter() stream = WatchedFileStream(self.stdout, time_format='%Y-%m-%d %H:%M:%S') self.assertTrue(isinstance(stream._time_format, str)) yield self.stop_arbiter() stream.close() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_timed_rotating_file_stream(self): yield self._start_arbiter() stream = TimedRotatingFileStream(self.stdout, rotate_when='H', rotate_interval='5', backup_count='3', utc='True') self.assertTrue(isinstance(stream._interval, int)) self.assertTrue(isinstance(stream._backup_count, int)) self.assertTrue(isinstance(stream._utc, bool)) self.assertTrue(stream._suffix is not None) self.assertTrue(stream._ext_match is not None) self.assertTrue(stream._rollover_at > 0) yield self.stop_arbiter() stream.close() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_stream(self): yield self._start_arbiter() # wait for the process to be started res1 = yield async_poll_for(self.stdout, 'stdout') res2 = yield async_poll_for(self.stderr, 'stderr') self.assertTrue(res1) self.assertTrue(res2) # clean slate truncate_file(self.stdout) truncate_file(self.stderr) # restart and make sure streams are still working yield self.restart_arbiter() # wait for the process to be restarted res1 = yield async_poll_for(self.stdout, 'stdout') res2 = yield async_poll_for(self.stderr, 'stderr') self.assertTrue(res1) self.assertTrue(res2) yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_pidfile.py000066400000000000000000000014151256046442300205510ustar00rootroot00000000000000import tempfile import os import subprocess from circus.pidfile import Pidfile from circus.tests.support import TestCase, EasyTestSuite, SLEEP class TestPidfile(TestCase): def test_pidfile(self): proc = subprocess.Popen(SLEEP % 120, shell=True) fd, path = tempfile.mkstemp() os.close(fd) try: pidfile = Pidfile(path) pidfile.create(proc.pid) self.assertRaises(RuntimeError, pidfile.create, proc.pid) pidfile.unlink() pidfile.create(proc.pid) pidfile.rename(path + '.2') self.assertTrue(os.path.exists(path + '.2')) self.assertFalse(os.path.exists(path)) finally: os.remove(path + '.2') test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_plugin_command_reloader.py000066400000000000000000000066621256046442300240170ustar00rootroot00000000000000from mock import patch from circus.plugins.command_reloader import CommandReloader from circus.tests.support import TestCircus, EasyTestSuite class TestCommandReloader(TestCircus): def setup_os_mock(self, realpath, mtime): patcher = patch('circus.plugins.command_reloader.os') os_mock = patcher.start() self.addCleanup(patcher.stop) os_mock.path.realpath.return_value = realpath os_mock.stat.return_value.st_mtime = mtime return os_mock def setup_call_mock(self, watcher_name): patcher = patch.object(CommandReloader, 'call') call_mock = patcher.start() self.addCleanup(patcher.stop) call_mock.side_effect = [ {'watchers': [watcher_name]}, {'options': {'cmd': watcher_name}}, None, ] return call_mock def test_default_loop_rate(self): plugin = self.make_plugin(CommandReloader, active=True) self.assertEqual(plugin.loop_rate, 1) def test_non_default_loop_rate(self): plugin = self.make_plugin(CommandReloader, active=True, loop_rate='2') self.assertEqual(plugin.loop_rate, 2) def test_mtime_is_modified(self): plugin = self.make_plugin(CommandReloader, active=True) plugin.cmd_files = {'foo': {'path': '/bar/baz', 'mtime': 1}} self.assertTrue(plugin.is_modified('foo', 2, '/bar/baz')) def test_path_is_modified(self): plugin = self.make_plugin(CommandReloader, active=True) plugin.cmd_files = {'foo': {'path': '/bar/baz', 'mtime': 1}} self.assertTrue(plugin.is_modified('foo', 1, '/bar/quux')) def test_not_modified(self): plugin = self.make_plugin(CommandReloader, active=True) plugin.cmd_files = {'foo': {'path': '/bar/quux', 'mtime': 1}} self.assertIs(plugin.is_modified('foo', 1, '/bar/quux'), False) def test_look_after_known_watcher_triggers_restart(self): call_mock = self.setup_call_mock(watcher_name='foo') self.setup_os_mock(realpath='/bar/foo', mtime=42) plugin = self.make_plugin(CommandReloader, active=True) plugin.cmd_files = {'foo': {'path': 'foo', 'mtime': 1}} plugin.look_after() self.assertEqual(plugin.cmd_files, { 'foo': {'path': '/bar/foo', 'mtime': 42} }) call_mock.assert_called_with('restart', name='foo') def test_look_after_new_watcher_does_not_restart(self): call_mock = self.setup_call_mock(watcher_name='foo') self.setup_os_mock(realpath='/bar/foo', mtime=42) plugin = self.make_plugin(CommandReloader, active=True) plugin.cmd_files = {} plugin.look_after() self.assertEqual(plugin.cmd_files, { 'foo': {'path': '/bar/foo', 'mtime': 42} }) # No restart, so last call should be for the 'get' command call_mock.assert_called_with('get', name='foo', keys=['cmd']) def test_missing_watcher_gets_removed_from_plugin_dict(self): self.setup_call_mock(watcher_name='bar') self.setup_os_mock(realpath='/bar/foo', mtime=42) plugin = self.make_plugin(CommandReloader, active=True) plugin.cmd_files = {'foo': {'path': 'foo', 'mtime': 1}} plugin.look_after() self.assertNotIn('foo', plugin.cmd_files) def test_handle_recv_implemented(self): plugin = self.make_plugin(CommandReloader, active=True) plugin.handle_recv('whatever') test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_plugin_flapping.py000066400000000000000000000046431256046442300223210ustar00rootroot00000000000000from mock import patch from circus.tests.support import TestCircus, EasyTestSuite from circus.plugins.flapping import Flapping class TestFlapping(TestCircus): def _flapping_plugin(self, **config): plugin = self.make_plugin(Flapping, active=True, **config) plugin.configs['test'] = {'active': True} plugin.timelines['test'] = [1, 2] return plugin def test_default_config(self): plugin = self._flapping_plugin() self.assertEqual(plugin.attempts, 2) self.assertEqual(plugin.window, 1) self.assertEqual(plugin.retry_in, 7) self.assertEqual(plugin.max_retry, 5) @patch.object(Flapping, 'check') def test_reap_message_calls_check(self, check_mock): plugin = self._flapping_plugin() topic = 'watcher.test.reap' plugin.handle_recv([topic, None]) check_mock.assert_called_with('test') @patch.object(Flapping, 'cast') @patch('circus.plugins.flapping.Timer') def test_below_max_retry_triggers_restart(self, timer_mock, cast_mock): plugin = self._flapping_plugin(max_retry=5) plugin.tries['test'] = 4 plugin.check('test') cast_mock.assert_called_with("stop", name="test") self.assertTrue(timer_mock.called) @patch.object(Flapping, 'cast') @patch('circus.plugins.flapping.Timer') def test_above_max_retry_triggers_final_stop(self, timer_mock, cast_mock): plugin = self._flapping_plugin(max_retry=5) plugin.tries['test'] = 5 plugin.check('test') cast_mock.assert_called_with("stop", name="test") self.assertFalse(timer_mock.called) def test_beyond_window_resets_tries(self): plugin = self._flapping_plugin(max_retry=-1) plugin.tries['test'] = 1 timestamp_beyond_window = plugin.window + plugin.check_delay + 1 plugin.timelines['test'] = [0, timestamp_beyond_window] plugin.check('test') self.assertEqual(plugin.tries['test'], 0) @patch.object(Flapping, 'cast') @patch('circus.plugins.flapping.Timer') def test_minus_one_max_retry_triggers_restart(self, timer_mock, cast_mock): plugin = self._flapping_plugin(max_retry=-1) plugin.timelines['test'] = [1, 2] plugin.tries['test'] = 5 plugin.check('test') cast_mock.assert_called_with("stop", name="test") self.assertTrue(timer_mock.called) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_plugin_resource_watcher.py000066400000000000000000000135621256046442300240650ustar00rootroot00000000000000import warnings from tornado.testing import gen_test from circus.tests.support import TestCircus, async_poll_for, Process from circus.tests.support import async_run_plugin, EasyTestSuite from circus.plugins.resource_watcher import ResourceWatcher # Make sure we don't allow more than 300MB in case things go wrong MAX_CHUNKS = 10000 CHUNK_SIZE = 30000 class Leaky(Process): def run(self): self._write('START') m = ' ' chunks_count = 0 while self.alive and chunks_count < MAX_CHUNKS: m += '*' * CHUNK_SIZE # for memory chunks_count += 1 self._write('STOP') def run_leaky(test_file): process = Leaky(test_file) process.run() return 1 fqn = 'circus.tests.test_plugin_resource_watcher.run_leaky' def get_statsd_increments(queue, plugin): queue.put(plugin.statsd.increments) class TestResourceWatcher(TestCircus): def _check_statsd(self, increments, name): res = list(increments.items()) self.assertTrue(len(res) > 0) for stat, items in res: if name == stat and items > 0: return raise AssertionError("%r stat not found" % name) def test_service_config_param_is_deprecated(self): with warnings.catch_warnings(record=True) as ws: # Cause all warnings to always be triggered. warnings.simplefilter("always") self.make_plugin(ResourceWatcher, service='whatever') self.assertTrue(any('ResourceWatcher' in w.message.args[0] for w in ws)) def test_watcher_config_param_is_required(self): self.assertRaises(NotImplementedError, self.make_plugin, ResourceWatcher) @gen_test def test_resource_watcher_max_mem(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.1, 'max_mem': 0.05, 'watcher': 'test'} kw = {'endpoint': self.arbiter.endpoint, 'pubsub_endpoint': self.arbiter.pubsub_endpoint} statsd_increments = yield async_run_plugin(ResourceWatcher, config, get_statsd_increments, **kw) self._check_statsd(statsd_increments, '_resource_watcher.test.over_memory') yield self.stop_arbiter() @gen_test def test_resource_watcher_max_mem_abs(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.1, 'max_mem': '1M', 'watcher': 'test'} kw = {'endpoint': self.arbiter.endpoint, 'pubsub_endpoint': self.arbiter.pubsub_endpoint} statsd_increments = yield async_run_plugin(ResourceWatcher, config, get_statsd_increments, **kw) self._check_statsd(statsd_increments, '_resource_watcher.test.over_memory') yield self.stop_arbiter() @gen_test def test_resource_watcher_min_mem(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.1, 'min_mem': 100000.1, 'watcher': 'test'} kw = {'endpoint': self.arbiter.endpoint, 'pubsub_endpoint': self.arbiter.pubsub_endpoint} statsd_increments = yield async_run_plugin(ResourceWatcher, config, get_statsd_increments, **kw) self._check_statsd(statsd_increments, '_resource_watcher.test.under_memory') yield self.stop_arbiter() @gen_test def test_resource_watcher_min_mem_abs(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.1, 'min_mem': '100M', 'watcher': 'test'} kw = {'endpoint': self.arbiter.endpoint, 'pubsub_endpoint': self.arbiter.pubsub_endpoint} statsd_increments = yield async_run_plugin(ResourceWatcher, config, get_statsd_increments, **kw) self._check_statsd(statsd_increments, '_resource_watcher.test.under_memory') yield self.stop_arbiter() @gen_test def test_resource_watcher_max_cpu(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.1, 'max_cpu': 0.1, 'watcher': 'test'} kw = {'endpoint': self.arbiter.endpoint, 'pubsub_endpoint': self.arbiter.pubsub_endpoint} statsd_increments = yield async_run_plugin(ResourceWatcher, config, get_statsd_increments, **kw) self._check_statsd(statsd_increments, '_resource_watcher.test.over_cpu') yield self.stop_arbiter() @gen_test def test_resource_watcher_min_cpu(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.1, 'min_cpu': 99.0, 'watcher': 'test'} kw = {'endpoint': self.arbiter.endpoint, 'pubsub_endpoint': self.arbiter.pubsub_endpoint} statsd_increments = yield async_run_plugin(ResourceWatcher, config, get_statsd_increments, **kw) self._check_statsd(statsd_increments, '_resource_watcher.test.under_cpu') yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_plugin_statsd.py000066400000000000000000000023451256046442300220200ustar00rootroot00000000000000from tornado.testing import gen_test from circus.tests.support import TestCircus, async_poll_for from circus.tests.support import async_run_plugin, EasyTestSuite from circus.plugins.statsd import FullStats def get_gauges(queue, plugin): queue.put(plugin.statsd.gauges) class TestFullStats(TestCircus): @gen_test def test_full_stats(self): dummy_process = 'circus.tests.support.run_process' yield self.start_arbiter(dummy_process) async_poll_for(self.test_file, 'START') config = {'loop_rate': 0.2} gauges = yield async_run_plugin( FullStats, config, plugin_info_callback=get_gauges, duration=1000, endpoint=self.arbiter.endpoint, pubsub_endpoint=self.arbiter.pubsub_endpoint) # we should have a bunch of stats events here self.assertTrue(len(gauges) >= 5) last_batch = sorted(name for name, value in gauges[-5:]) wanted = ['_stats.test.cpu_sum', '_stats.test.mem_max', '_stats.test.mem_pct_max', '_stats.test.mem_pct_sum', '_stats.test.mem_sum'] self.assertEqual(last_batch, wanted) yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_plugin_watchdog.py000066400000000000000000000045641256046442300223230ustar00rootroot00000000000000import socket import time import os import warnings from tornado.testing import gen_test from circus.tests.support import TestCircus, Process, async_poll_for from circus.tests.support import async_run_plugin as arp, EasyTestSuite from circus.plugins.watchdog import WatchDog class DummyWatchDogged(Process): def run(self): self._write('STARTWD') sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP try: my_pid = os.getpid() for _ in range(5): message = "{pid};{time}".format(pid=my_pid, time=time.time()) sock.sendto(message.encode('utf-8'), ('127.0.0.1', 1664)) time.sleep(0.5) self._write('STOPWD') finally: sock.close() def run_dummy_watchdogged(test_file): process = DummyWatchDogged(test_file) process.run() return 1 def get_pid_status(queue, plugin): queue.put(plugin.pid_status) fqn = 'circus.tests.test_plugin_watchdog.run_dummy_watchdogged' class TestPluginWatchDog(TestCircus): @gen_test def test_watchdog_discovery_found(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'STARTWD') pubsub = self.arbiter.pubsub_endpoint config = {'loop_rate': 0.1, 'watchers_regex': "^test.*$"} with warnings.catch_warnings(): pid_status = yield arp(WatchDog, config, get_pid_status, endpoint=self.arbiter.endpoint, pubsub_endpoint=pubsub) self.assertEqual(len(pid_status), 1, pid_status) yield self.stop_arbiter() async_poll_for(self.test_file, 'STOPWD') @gen_test def test_watchdog_discovery_not_found(self): yield self.start_arbiter(fqn) async_poll_for(self.test_file, 'START') pubsub = self.arbiter.pubsub_endpoint config = {'loop_rate': 0.1, 'watchers_regex': "^foo.*$"} with warnings.catch_warnings(): pid_status = yield arp(WatchDog, config, get_pid_status, endpoint=self.arbiter.endpoint, pubsub_endpoint=pubsub) self.assertEqual(len(pid_status), 0, pid_status) yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_process.py000066400000000000000000000143011256046442300206110ustar00rootroot00000000000000import os import sys import time from circus.process import Process from circus.tests.support import (TestCircus, skipIf, EasyTestSuite, DEBUG, poll_for, IS_WINDOWS, PYTHON, SLEEP) import circus.py3compat from circus.py3compat import StringIO, PY2 RLIMIT = """\ import resource, sys try: with open(sys.argv[1], 'w') as f: for limit in ('NOFILE', 'NPROC'): res = getattr(resource, 'RLIMIT_%s' % limit) f.write('%s=%s\\n' % (limit, resource.getrlimit(res))) f.write('END') finally: sys.exit(0) """ VERBOSE = """\ import sys try: for i in range(1000): for stream in (sys.stdout, sys.stderr): stream.write(str(i)) stream.flush() with open(sys.argv[1], 'w') as f: f.write('END') finally: sys.exit(0) """ # On Windows we can't close the fds if we are # redirecting stdout or stderr USE_FDS = IS_WINDOWS def _nose_no_s(): if PY2: return not hasattr(sys.stdout, 'fileno') else: return isinstance(sys.stdout, StringIO) class TestProcess(TestCircus): def test_base(self): cmd = PYTHON args = "-c 'import time; time.sleep(10)'" process = Process('test', 1, cmd, args=args, shell=False, use_fds=USE_FDS) try: info = process.info() self.assertEqual(process.pid, info['pid']) # Make sure the process lived a measurable amount of time # (precision error on Windows) time.sleep(0.01) age = process.age() self.assertTrue(age > 0.) self.assertFalse(process.is_child(0)) finally: process.stop() @skipIf(DEBUG, 'Py_DEBUG=1') @skipIf(IS_WINDOWS, "RLIMIT is not supported on Windows") def test_rlimits(self): script_file = self.get_tmpfile(RLIMIT) output_file = self.get_tmpfile() cmd = PYTHON args = [script_file, output_file] rlimits = {'nofile': 20, 'nproc': 20} process = Process('test', 1, cmd, args=args, rlimits=rlimits) poll_for(output_file, 'END') process.stop() with open(output_file, 'r') as f: output = {} for line in f.readlines(): line = line.rstrip() line = line.split('=', 1) if len(line) != 2: continue limit, value = line output[limit] = value def srt2ints(val): return [circus.py3compat.long(key) for key in val[1:-1].split(',')] wanted = [circus.py3compat.long(20), circus.py3compat.long(20)] self.assertEqual(srt2ints(output['NOFILE']), wanted) self.assertEqual(srt2ints(output['NPROC']), wanted) def test_comparison(self): cmd = PYTHON args = ['import time; time.sleep(2)', ] p1 = Process('test', 1, cmd, args=args, use_fds=USE_FDS) # Make sure the two processes are launched with a measurable # difference. (precsion error on Windows) time.sleep(0.01) p2 = Process('test', 2, cmd, args=args, use_fds=USE_FDS) self.assertTrue(p1 < p2) self.assertFalse(p1 == p2) self.assertTrue(p1 == p1) p1.stop() p2.stop() def test_process_parameters(self): # all the options passed to the process should be available by the # command / process p1 = Process('test', 1, 'make-me-a-coffee', '$(circus.wid) --type $(circus.env.type)', shell=False, spawn=False, env={'type': 'macchiato'}, use_fds=USE_FDS) self.assertEqual(['make-me-a-coffee', '1', '--type', 'macchiato'], p1.format_args()) p2 = Process('test', 1, 'yeah $(CIRCUS.WID)', spawn=False, use_fds=USE_FDS) self.assertEqual(['yeah', '1'], p2.format_args()) os.environ['coffee_type'] = 'american' p3 = Process('test', 1, 'yeah $(circus.env.type)', shell=False, spawn=False, env={'type': 'macchiato'}, use_fds=USE_FDS) self.assertEqual(['yeah', 'macchiato'], p3.format_args()) os.environ.pop('coffee_type') @skipIf(DEBUG, 'Py_DEBUG=1') @skipIf(_nose_no_s(), 'Nose runs without -s') @skipIf(IS_WINDOWS, "Streams not supported") def test_streams(self): script_file = self.get_tmpfile(VERBOSE) output_file = self.get_tmpfile() cmd = PYTHON args = [script_file, output_file] # 1. streams sent to /dev/null process = Process('test', 1, cmd, args=args, close_child_stdout=True, close_child_stderr=True) try: poll_for(output_file, 'END') # the pipes should be empty self.assertEqual(process.stdout.read(), b'') self.assertEqual(process.stderr.read(), b'') finally: process.stop() # 2. streams sent to /dev/null, no PIPEs output_file = self.get_tmpfile() args[1] = output_file process = Process('test', 1, cmd, args=args, close_child_stdout=True, close_child_stderr=True, pipe_stdout=False, pipe_stderr=False) try: poll_for(output_file, 'END') # the pipes should be unexistant self.assertTrue(process.stdout is None) self.assertTrue(process.stderr is None) finally: process.stop() # 3. streams & pipes open output_file = self.get_tmpfile() args[1] = output_file process = Process('test', '1', cmd, args=args) try: poll_for(output_file, 'END') # the pipes should be unexistant self.assertEqual(len(process.stdout.read()), 2890) self.assertEqual(len(process.stderr.read()), 2890) finally: process.stop() @skipIf(IS_WINDOWS, "No GID on Windows") def test_initgroups(self): cmd = sys.executable args = [SLEEP % 2] gid = os.getgid() uid = os.getuid() p1 = Process('test', '1', cmd, args=args, gid=gid, uid=uid) p1.stop() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_reloadconfig.py000066400000000000000000000146351256046442300216010ustar00rootroot00000000000000import os import tornado import tornado.testing from circus.arbiter import Arbiter from circus.tests.support import EasyTestSuite HERE = os.path.join(os.path.dirname(__file__)) CONFIG_DIR = os.path.join(HERE, 'config') _CONF = { 'reload_base': os.path.join(CONFIG_DIR, 'reload_base.ini'), 'reload_numprocesses': os.path.join(CONFIG_DIR, 'reload_numprocesses.ini'), 'reload_addwatchers': os.path.join(CONFIG_DIR, 'reload_addwatchers.ini'), 'reload_delwatchers': os.path.join(CONFIG_DIR, 'reload_delwatchers.ini'), 'reload_changewatchers': os.path.join(CONFIG_DIR, 'reload_changewatchers.ini'), 'reload_addplugins': os.path.join(CONFIG_DIR, 'reload_addplugins.ini'), 'reload_delplugins': os.path.join(CONFIG_DIR, 'reload_delplugins.ini'), 'reload_changeplugins': os.path.join(CONFIG_DIR, 'reload_changeplugins.ini'), 'reload_addsockets': os.path.join(CONFIG_DIR, 'reload_addsockets.ini'), 'reload_delsockets': os.path.join(CONFIG_DIR, 'reload_delsockets.ini'), 'reload_changesockets': os.path.join(CONFIG_DIR, 'reload_changesockets.ini'), 'reload_changearbiter': os.path.join(CONFIG_DIR, 'reload_changearbiter.ini'), 'reload_statsd': os.path.join(CONFIG_DIR, 'reload_statsd.ini'), } class FakeSocket(object): closed = False def send_multipart(self, *args): pass close = send_multipart class TestConfig(tornado.testing.AsyncTestCase): def setUp(self): super(TestConfig, self).setUp() self.a = self._load_base_arbiter() @tornado.gen.coroutine def _tearDown(self): yield self._tear_down_arbiter(self.a) @tornado.gen.coroutine def _tear_down_arbiter(self, a): for watcher in a.iter_watchers(): yield watcher._stop() a.sockets.close_all() def get_new_ioloop(self): return tornado.ioloop.IOLoop.instance() def _load_base_arbiter(self, name='reload_base'): loop = tornado.ioloop.IOLoop.instance() a = Arbiter.load_from_config(_CONF[name], loop=loop) a.evpub_socket = FakeSocket() # initialize watchers for watcher in a.iter_watchers(): a._watchers_names[watcher.name.lower()] = watcher return a def test_watcher_names(self): watcher_names = sorted(i.name for i in self.a.watchers) self.assertEqual(watcher_names, ['plugin:myplugin', 'test1', 'test2']) @tornado.testing.gen_test def test_reload_numprocesses(self): w = self.a.get_watcher('test1') self.assertEqual(w.numprocesses, 1) yield self.a.reload_from_config(_CONF['reload_numprocesses']) self.assertEqual(w.numprocesses, 2) yield self._tearDown() @tornado.testing.gen_test def test_reload_addwatchers(self): self.assertEqual(len(self.a.watchers), 3) yield self.a.reload_from_config(_CONF['reload_addwatchers']) self.assertEqual(len(self.a.watchers), 4) yield self._tearDown() @tornado.testing.gen_test def test_reload_delwatchers(self): self.assertEqual(len(self.a.watchers), 3) yield self.a.reload_from_config(_CONF['reload_delwatchers']) self.assertEqual(len(self.a.watchers), 2) yield self._tearDown() @tornado.testing.gen_test def test_reload_changewatchers(self): self.assertEqual(len(self.a.watchers), 3) w0 = self.a.get_watcher('test1') w1 = self.a.get_watcher('test2') yield self.a.reload_from_config(_CONF['reload_changewatchers']) self.assertEqual(len(self.a.watchers), 3) self.assertEqual(self.a.get_watcher('test1'), w0) self.assertNotEqual(self.a.get_watcher('test2'), w1) yield self._tearDown() @tornado.testing.gen_test def test_reload_addplugins(self): self.assertEqual(len(self.a.watchers), 3) yield self.a.reload_from_config(_CONF['reload_addplugins']) self.assertEqual(len(self.a.watchers), 4) yield self._tearDown() @tornado.testing.gen_test def test_reload_delplugins(self): self.assertEqual(len(self.a.watchers), 3) yield self.a.reload_from_config(_CONF['reload_delplugins']) self.assertEqual(len(self.a.watchers), 2) yield self._tearDown() @tornado.testing.gen_test def test_reload_changeplugins(self): self.assertEqual(len(self.a.watchers), 3) p = self.a.get_watcher('plugin:myplugin') yield self.a.reload_from_config(_CONF['reload_changeplugins']) self.assertEqual(len(self.a.watchers), 3) self.assertNotEqual(self.a.get_watcher('plugin:myplugin'), p) yield self._tearDown() @tornado.testing.gen_test def test_reload_addsockets(self): self.assertEqual(len(self.a.sockets), 1) yield self.a.reload_from_config(_CONF['reload_addsockets']) self.assertEqual(len(self.a.sockets), 2) yield self._tearDown() @tornado.testing.gen_test def test_reload_delsockets(self): self.assertEqual(len(self.a.sockets), 1) yield self.a.reload_from_config(_CONF['reload_delsockets']) self.assertEqual(len(self.a.sockets), 0) yield self._tearDown() @tornado.testing.gen_test def test_reload_changesockets(self): self.assertEqual(len(self.a.sockets), 1) s = self.a.get_socket('mysocket') yield self.a.reload_from_config(_CONF['reload_changesockets']) self.assertEqual(len(self.a.sockets), 1) self.assertNotEqual(self.a.get_socket('mysocket'), s) yield self._tearDown() @tornado.testing.gen_test def test_reload_envdictparsed(self): # environ var that needs a `circus.util.parse_env_dict` treatment os.environ['SHRUBBERY'] = ' NI ' a = self._load_base_arbiter() try: w = a.get_watcher('test1') yield a.reload_from_config(_CONF['reload_base']) self.assertEqual(a.get_watcher('test1'), w) finally: del os.environ['SHRUBBERY'] yield self._tear_down_arbiter(a) @tornado.testing.gen_test def test_reload_ignorearbiterwatchers(self): a = self._load_base_arbiter('reload_statsd') statsd = a.get_watcher('circusd-stats') yield a.reload_from_config(_CONF['reload_statsd']) self.assertEqual(statsd, a.get_watcher('circusd-stats')) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_runner.py000066400000000000000000000007611256046442300204510ustar00rootroot00000000000000from tornado.testing import gen_test from circus.tests.support import TestCircus, async_poll_for, EasyTestSuite def Dummy(test_file): with open(test_file, 'w') as f: f.write('..........') return 1 class TestRunner(TestCircus): @gen_test def test_dummy(self): yield self.start_arbiter('circus.tests.test_runner.Dummy') self.assertTrue(async_poll_for(self.test_file, '..........')) yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_sighandler.py000066400000000000000000000010371256046442300212550ustar00rootroot00000000000000from tornado.testing import gen_test from circus.tests.support import TestCircus, async_poll_for, EasyTestSuite class TestSigHandler(TestCircus): @gen_test def test_handler(self): yield self.start_arbiter() # wait for the process to be started self.assertTrue(async_poll_for(self.test_file, 'START')) # stopping... yield self.arbiter.stop() # wait for the process to be stopped self.assertTrue(async_poll_for(self.test_file, 'QUIT')) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_sockets.py000066400000000000000000000151651256046442300206170ustar00rootroot00000000000000import os import socket import tempfile try: import IN except ImportError: pass import mock from circus.tests.support import TestCase, skipIf, EasyTestSuite, IS_WINDOWS from circus.sockets import CircusSocket, CircusSockets def so_bindtodevice_supported(): try: if hasattr(IN, 'SO_BINDTODEVICE'): return True except NameError: pass return False class TestSockets(TestCase): def setUp(self): super(TestSockets, self).setUp() self.files = [] def tearDown(self): for file_ in self.files: if os.path.exists(file_): os.remove(file_) super(TestSockets, self).tearDown() def _get_file(self): fd, _file = tempfile.mkstemp() os.close(fd) self.files.append(_file) return _file def _get_tmp_filename(self): # XXX horrible way to get a filename fd, _file = tempfile.mkstemp() os.close(fd) os.remove(_file) return _file def test_socket(self): sock = CircusSocket('somename', 'localhost', 0) try: sock.bind_and_listen() finally: sock.close() def test_manager(self): mgr = CircusSockets() for i in range(5): mgr.add(str(i), 'localhost', 0) port = mgr['1'].port try: mgr.bind_and_listen_all() # we should have a port now self.assertNotEqual(port, mgr['1'].port) finally: mgr.close_all() def test_load_from_config_no_proto(self): """When no proto in the config, the default (0) is used.""" config = {'name': ''} sock = CircusSocket.load_from_config(config) self.assertEqual(sock.proto, 0) sock.close() def test_load_from_config_unknown_proto(self): """Unknown proto in the config raises an error.""" config = {'name': '', 'proto': 'foo'} self.assertRaises(socket.error, CircusSocket.load_from_config, config) @skipIf(IS_WINDOWS, "Unix sockets not supported on this platform") def test_load_from_config_umask(self): sockfile = self._get_tmp_filename() config = {'name': 'somename', 'path': sockfile, 'umask': 0} sock = CircusSocket.load_from_config(config) try: self.assertEqual(sock.umask, 0) finally: sock.close() @skipIf(IS_WINDOWS, "Unix sockets not supported on this platform") def test_load_from_config_replace(self): sockfile = self._get_file() config = {'name': 'somename', 'path': sockfile, 'replace': False} sock = CircusSocket.load_from_config(config) try: self.assertRaises(OSError, sock.bind_and_listen) finally: sock.close() config = {'name': 'somename', 'path': sockfile, 'replace': True} sock = CircusSocket.load_from_config(config) sock.bind_and_listen() try: self.assertEqual(sock.replace, True) finally: sock.close() @skipIf(IS_WINDOWS, "Unix sockets not supported on this platform") def test_unix_socket(self): sockfile = self._get_tmp_filename() sock = CircusSocket('somename', path=sockfile, umask=0) try: sock.bind_and_listen() self.assertTrue(os.path.exists(sockfile)) permissions = oct(os.stat(sockfile).st_mode)[-3:] self.assertEqual(permissions, '777') finally: sock.close() @skipIf(IS_WINDOWS, "Unix sockets not supported on this platform") def test_unix_cleanup(self): sockets = CircusSockets() sockfile = self._get_tmp_filename() try: sockets.add('unix', path=sockfile) sockets.bind_and_listen_all() self.assertTrue(os.path.exists(sockfile)) finally: sockets.close_all() self.assertTrue(not os.path.exists(sockfile)) @skipIf(not so_bindtodevice_supported(), 'SO_BINDTODEVICE unsupported') def test_bind_to_interface(self): config = {'name': '', 'host': 'localhost', 'port': 0, 'interface': 'lo'} sock = CircusSocket.load_from_config(config) self.assertEqual(sock.interface, config['interface']) sock.setsockopt = mock.Mock() try: sock.bind_and_listen() sock.setsockopt.assert_any_call(socket.SOL_SOCKET, IN.SO_BINDTODEVICE, config['interface'] + '\0') finally: sock.close() def test_inet6(self): config = {'name': '', 'host': '::1', 'port': 0, 'family': 'AF_INET6'} sock = CircusSocket.load_from_config(config) self.assertEqual(sock.host, config['host']) self.assertEqual(sock.port, config['port']) sock.setsockopt = mock.Mock() try: sock.bind_and_listen() # we should have got a port set self.assertNotEqual(sock.port, 0) finally: sock.close() @skipIf(not hasattr(socket, 'SO_REUSEPORT'), 'socket.SO_REUSEPORT unsupported') def test_reuseport_supported(self): config = {'name': '', 'host': 'localhost', 'port': 0, 'so_reuseport': True} sock = CircusSocket.load_from_config(config) try: sockopt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) except socket.error: # see #699 return finally: sock.close() self.assertEqual(sock.so_reuseport, True) self.assertNotEqual(sockopt, 0) def test_reuseport_unsupported(self): config = {'name': '', 'host': 'localhost', 'port': 0, 'so_reuseport': True} saved = None try: if hasattr(socket, 'SO_REUSEPORT'): saved = socket.SO_REUSEPORT del socket.SO_REUSEPORT sock = CircusSocket.load_from_config(config) self.assertEqual(sock.so_reuseport, False) finally: if saved is not None: socket.SO_REUSEPORT = saved sock.close() @skipIf(not hasattr(os, 'set_inheritable'), 'os.set_inheritable unsupported') @skipIf(IS_WINDOWS, "Unix sockets not supported on this platform") def test_set_inheritable(self): sockfile = self._get_tmp_filename() sock = CircusSocket('somename', path=sockfile, umask=0) try: sock.bind_and_listen() self.assertTrue(sock.get_inheritable()) finally: sock.close() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_stats_client.py000066400000000000000000000056151256046442300216370ustar00rootroot00000000000000import time import tempfile import os import sys import tornado from circus.tests.support import TestCircus, EasyTestSuite, skipIf, IS_WINDOWS from circus.client import AsyncCircusClient from circus.stream import FileStream from circus.py3compat import get_next from circus.util import tornado_sleep def run_process(*args, **kw): try: i = 0 while True: sys.stdout.write('%.2f-stdout-%d-%s\n' % (time.time(), os.getpid(), i)) sys.stdout.flush() sys.stderr.write('%.2f-stderr-%d-%s\n' % (time.time(), os.getpid(), i)) sys.stderr.flush() time.sleep(.25) except: return 1 class TestStatsClient(TestCircus): def setUp(self): super(TestStatsClient, self).setUp() self.files = [] def _get_file(self): fd, log = tempfile.mkstemp() os.close(fd) self.files.append(log) return log def tearDown(self): super(TestStatsClient, self).tearDown() for file in self.files: if os.path.exists(file): os.remove(file) @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_handler(self): log = self._get_file() stream = {'stream': FileStream(log)} cmd = 'circus.tests.test_stats_client.run_process' stdout_stream = stream stderr_stream = stream yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream, stderr_stream=stderr_stream, stats=True, debug=False) # waiting for data to appear in the file stream empty = True while empty: with open(log) as f: empty = f.read() == '' yield tornado_sleep(.1) # checking that our system is live and running client = AsyncCircusClient(endpoint=self.arbiter.endpoint) res = yield client.send_message('list') watchers = sorted(res['watchers']) self.assertEqual(['circusd-stats', 'test'], watchers) # making sure the stats process run res = yield client.send_message('status', name='test') self.assertEqual(res['status'], 'active') res = yield client.send_message('status', name='circusd-stats') self.assertEqual(res['status'], 'active') # playing around with the stats now: we should get some ! from circus.stats.client import StatsClient client = StatsClient(endpoint=self.arbiter.stats_endpoint) next = get_next(client.iter_messages()) for i in range(10): watcher, pid, stat = next() self.assertTrue(watcher in ('test', 'circusd-stats', 'circus'), watcher) yield self.stop_arbiter() test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_stats_collector.py000066400000000000000000000143231256046442300223430ustar00rootroot00000000000000import socket import time from collections import defaultdict from circus.fixed_threading import Thread from zmq.eventloop import ioloop from circus.stats import collector as collector_module from circus.stats.collector import SocketStatsCollector, WatcherStatsCollector from circus.tests.support import TestCase, EasyTestSuite class TestCollector(TestCase): def setUp(self): # let's create 10 sockets and their clients self.socks = [] self.clients = [] self.fds = [] self.pids = {} def tearDown(self): for sock, _, _ in self.socks: sock.close() for sock in self.clients: sock.close() def _get_streamer(self): for i in range(10): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('localhost', 0)) sock.listen(1) self.socks.append((sock, 'localhost:0', sock.fileno())) self.fds.append(sock.fileno()) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(sock.getsockname()) self.clients.append(client) class FakeStreamer(object): stats = [] def __init__(this): this.sockets = self.socks @property def circus_pids(this): return self.circus_pids def get_pids(this, name): return self.pids[name] @property def publisher(this): return this def publish(this, name, stat): this.stats.append(stat) self.streamer = FakeStreamer() return self.streamer def _get_collector(self, collector_class): self._get_streamer() class Collector(Thread): def __init__(this, streamer): Thread.__init__(this) this.streamer = streamer this.loop = ioloop.IOLoop() this.daemon = True def run(self): collector = collector_class( self.streamer, 'sockets', callback_time=0.1, io_loop=self.loop) collector.start() self.loop.start() def stop(self): self.loop.add_callback(self.loop.stop) self.loop.add_callback(self.loop.close) return Collector(self.streamer) def test_watcherstats(self): calls = defaultdict(int) info = [] for i in range(2): info.append({ 'age': 154058.91111397743 + i, 'children': [], 'cmdline': 'python', 'cpu': 0.0 + i / 10., 'create_time': 1378663281.96, 'ctime': '0:00.0', 'mem': 0.0, 'mem_info1': '52K', 'mem_info2': '39M', 'nice': 0, 'pid': None, 'username': 'alexis'}) def _get_info(pid): try: data = info[calls[pid]].copy() except IndexError: raise collector_module.util.NoSuchProcess(pid) data['pid'] = pid calls[pid] += 1 return data old_info = collector_module.util.get_info try: collector_module.util.get_info = _get_info self.pids['firefox'] = [2353, 2354] collector = WatcherStatsCollector(self._get_streamer(), 'firefox') stats = list(collector.collect_stats()) self.assertEqual(len(stats), 3) stats = list(collector.collect_stats()) self.assertEqual(len(stats), 3) stats = list(collector.collect_stats()) self.assertEqual(len(stats), 1) self.circus_pids = {1234: 'ohyeah'} self.pids['circus'] = [1234] collector = WatcherStatsCollector(self._get_streamer(), 'circus') stats = list(collector.collect_stats()) self.assertEqual(stats[0]['name'], 'ohyeah') finally: collector_module.util.get_info = old_info def test_collector_aggregation(self): collector = WatcherStatsCollector(self._get_streamer(), 'firefox') aggregate = {} for i in range(0, 10): pid = 1000 + i aggregate[pid] = { 'age': 154058.91111397743, 'children': [], 'cmdline': 'python', 'cpu': 0.0 + i / 10., 'create_time': 1378663281.96, 'ctime': '0:00.0', 'mem': 0.0 + i // 10, 'mem_info1': '52K', 'mem_info2': '39M', 'username': 'alexis', 'subtopic': pid, 'name': 'firefox'} res = collector._aggregate(aggregate) self.assertEqual(res['mem'], 0) self.assertEqual(len(res['pid']), 10) self.assertEqual(res['cpu'], 0.45) def test_collector_aggregation_when_unknown_values(self): collector = WatcherStatsCollector(self._get_streamer(), 'firefox') aggregate = {} for i in range(0, 10): pid = 1000 + i aggregate[pid] = { 'age': 'N/A', 'children': [], 'cmdline': 'python', 'cpu': 'N/A', 'create_time': 1378663281.96, 'ctime': '0:00.0', 'mem': 'N/A', 'mem_info1': '52K', 'mem_info2': '39M', 'nice': 0, 'pid': pid, 'username': 'alexis', 'subtopic': pid, 'name': 'firefox'} res = collector._aggregate(aggregate) self.assertEqual(res['mem'], 'N/A') self.assertEqual(len(res['pid']), 10) self.assertEqual(res['cpu'], 'N/A') def test_socketstats(self): collector = self._get_collector(SocketStatsCollector) collector.start() time.sleep(1.) # doing some socket things as a client for i in range(10): for client in self.clients: client.send(b'ok') # client.recv(2) # stopping collector.stop() for s, _, _ in self.socks: s.close() # let's see what we got self.assertTrue(len(self.streamer.stats) > 2) stat = self.streamer.stats[0] self.assertTrue(stat['fd'] in self.fds) self.assertTrue(stat['reads'] > 1) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_stats_publisher.py000066400000000000000000000024031256046442300223460ustar00rootroot00000000000000import mock import zmq import zmq.utils.jsonapi as json from circus.tests.support import TestCase, EasyTestSuite from circus.stats.publisher import StatsPublisher class TestStatsPublisher(TestCase): def test_publish(self): publisher = StatsPublisher() publisher.socket.close() publisher.socket = mock.MagicMock() stat = {'subtopic': 1, 'foo': 'bar'} publisher.publish('foobar', stat) publisher.socket.send_multipart.assert_called_with( [b'stat.foobar.1', json.dumps(stat)]) def test_publish_reraise_zmq_errors(self): publisher = StatsPublisher() publisher.socket = mock.MagicMock() publisher.socket.closed = False publisher.socket.send_multipart.side_effect = zmq.ZMQError() stat = {'subtopic': 1, 'foo': 'bar'} self.assertRaises(zmq.ZMQError, publisher.publish, 'foobar', stat) def test_publish_silent_zmq_errors_when_socket_closed(self): publisher = StatsPublisher() publisher.socket = mock.MagicMock() publisher.socket.closed = True publisher.socket.send_multipart.side_effect = zmq.ZMQError() stat = {'subtopic': 1, 'foo': 'bar'} publisher.publish('foobar', stat) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_stats_streamer.py000066400000000000000000000061371256046442300222030ustar00rootroot00000000000000import os import tempfile import mock from circus.tests.support import TestCircus, EasyTestSuite from circus.stats.streamer import StatsStreamer from circus import client class _StatsStreamer(StatsStreamer): msgs = [] def handle_recv(self, data): self.msgs.append(data) class FakeStreamer(StatsStreamer): def __init__(self, *args, **kwargs): self._initialize() class TestStatsStreamer(TestCircus): def setUp(self): self.old = client.CircusClient.call client.CircusClient.call = self._call fd, self._unix = tempfile.mkstemp() os.close(fd) def tearDown(self): client.CircusClient.call = self.old os.remove(self._unix) def _call(self, cmd): what = cmd['command'] if what == 'list': name = cmd['properties'].get('name') if name is None: return {'watchers': ['one', 'two', 'three']} return {'pids': [123, 456]} elif what == 'dstats': return {'info': {'pid': 789}} elif what == 'listsockets': return {'status': 'ok', 'sockets': [{'path': self._unix, 'fd': 5, 'name': 'XXXX', 'backlog': 2048}], 'time': 1369647058.967524} raise NotImplementedError(cmd) def test_get_pids_circus(self): streamer = FakeStreamer() streamer.circus_pids = {1234: 'circus-top', 1235: 'circusd'} self.assertEqual(streamer.get_pids('circus'), [1234, 1235]) def test_get_pids(self): streamer = FakeStreamer() streamer._pids['foobar'] = [1234, 1235] self.assertEqual(streamer.get_pids('foobar'), [1234, 1235]) def test_get_all_pids(self): streamer = FakeStreamer() streamer._pids['foobar'] = [1234, 1235] streamer._pids['barbaz'] = [1236, 1237] self.assertEqual(set(streamer.get_pids()), set([1234, 1235, 1236, 1237])) @mock.patch('os.getpid', lambda: 2222) def test_get_circus_pids(self): def _send_message(message, name=None): if message == 'list': if name == 'circushttpd': return {'pids': [3333]} return {'watchers': ['circushttpd']} if message == 'dstats': return {'info': {'pid': 1111}} streamer = FakeStreamer() streamer.client = mock.MagicMock() streamer.client.send_message = _send_message self.assertEqual( streamer.get_circus_pids(), {1111: 'circusd', 2222: 'circusd-stats', 3333: 'circushttpd'}) def test_remove_pid(self): streamer = FakeStreamer() streamer._callbacks['foobar'] = mock.MagicMock() streamer._pids = {'foobar': [1234, 1235]} streamer.remove_pid('foobar', 1234) self.assertFalse(streamer._callbacks['foobar'].stop.called) streamer.remove_pid('foobar', 1235) self.assertTrue(streamer._callbacks['foobar'].stop.called) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_stream.py000066400000000000000000000301561256046442300204340ustar00rootroot00000000000000import time import sys import os import tempfile import tornado from datetime import datetime from circus.py3compat import StringIO from circus.client import make_message from circus.tests.support import TestCircus, async_poll_for, truncate_file from circus.tests.support import TestCase, EasyTestSuite, skipIf, IS_WINDOWS from circus.stream import FileStream, WatchedFileStream from circus.stream import TimedRotatingFileStream from circus.stream import FancyStdoutStream def run_process(testfile, *args, **kw): try: # print once, then wait sys.stdout.write('stdout') sys.stdout.flush() sys.stderr.write('stderr') sys.stderr.flush() with open(testfile, 'a+') as f: f.write('START') time.sleep(1.) except: return 1 class TestWatcher(TestCircus): dummy_process = 'circus.tests.test_stream.run_process' def setUp(self): super(TestWatcher, self).setUp() fd, self.stdout = tempfile.mkstemp() os.close(fd) fd, self.stderr = tempfile.mkstemp() os.close(fd) self.stdout_stream = FileStream(self.stdout) self.stderr_stream = FileStream(self.stderr) self.stdout_arg = {'stream': self.stdout_stream} self.stderr_arg = {'stream': self.stderr_stream} def tearDown(self): self.stdout_stream.close() self.stderr_stream.close() if os.path.exists(self.stdout): os.remove(self.stdout) if os.path.exists(self.stderr): os.remove(self.stderr) @tornado.gen.coroutine def _start_arbiter(self): yield self.start_arbiter(cmd=self.dummy_process, stdout_stream=self.stdout_arg, stderr_stream=self.stderr_arg) @tornado.gen.coroutine def restart_arbiter(self): yield self.arbiter.restart() @tornado.gen.coroutine def call(self, _cmd, **props): msg = make_message(_cmd, **props) resp = yield self.cli.call(msg) raise tornado.gen.Return(resp) @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_file_stream(self): yield self._start_arbiter() stream = FileStream(self.stdout, max_bytes='12', backup_count='3') self.assertTrue(isinstance(stream._max_bytes, int)) self.assertTrue(isinstance(stream._backup_count, int)) yield self.stop_arbiter() stream.close() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_watched_file_stream(self): yield self._start_arbiter() stream = WatchedFileStream(self.stdout, time_format='%Y-%m-%d %H:%M:%S') self.assertTrue(isinstance(stream._time_format, str)) yield self.stop_arbiter() stream.close() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_timed_rotating_file_stream(self): yield self._start_arbiter() stream = TimedRotatingFileStream(self.stdout, rotate_when='H', rotate_interval='5', backup_count='3', utc='True') self.assertTrue(isinstance(stream._interval, int)) self.assertTrue(isinstance(stream._backup_count, int)) self.assertTrue(isinstance(stream._utc, bool)) self.assertTrue(stream._suffix is not None) self.assertTrue(stream._ext_match is not None) self.assertTrue(stream._rollover_at > 0) yield self.stop_arbiter() stream.close() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_stream(self): yield self._start_arbiter() # wait for the process to be started res1 = yield async_poll_for(self.stdout, 'stdout') res2 = yield async_poll_for(self.stderr, 'stderr') self.assertTrue(res1) self.assertTrue(res2) # clean slate truncate_file(self.stdout) truncate_file(self.stderr) # restart and make sure streams are still working yield self.restart_arbiter() # wait for the process to be restarted res1 = yield async_poll_for(self.stdout, 'stdout') res2 = yield async_poll_for(self.stderr, 'stderr') self.assertTrue(res1) self.assertTrue(res2) yield self.stop_arbiter() @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_stop_and_restart(self): # cf https://github.com/circus-tent/circus/issues/912 yield self._start_arbiter() # wait for the process to be started res1 = yield async_poll_for(self.stdout, 'stdout') res2 = yield async_poll_for(self.stderr, 'stderr') self.assertTrue(res1) self.assertTrue(res2) self.assertFalse(self.stdout_stream._file.closed) self.assertFalse(self.stderr_stream._file.closed) # clean slate truncate_file(self.stdout) truncate_file(self.stderr) # stop the watcher yield self.arbiter.watchers[0].stop() self.assertTrue(self.stdout_stream._file.closed) self.assertTrue(self.stderr_stream._file.closed) # start it again yield self.arbiter.watchers[0].start() # wait for the process to be restarted res1 = yield async_poll_for(self.stdout, 'stdout') res2 = yield async_poll_for(self.stderr, 'stderr') self.assertTrue(res1) self.assertTrue(res2) self.assertFalse(self.stdout_stream._file.closed) self.assertFalse(self.stderr_stream._file.closed) yield self.stop_arbiter() class TestFancyStdoutStream(TestCase): def color_start(self, code): return '\033[0;3%s;40m' % code def color_end(self): return '\033[0m\n' def get_stream(self, *args, **kw): # need a constant timestamp now = datetime.now() stream = FancyStdoutStream(*args, **kw) # patch some details that will be used stream.out = StringIO() stream.now = lambda: now return stream def get_output(self, stream): # stub data data = {'data': 'hello world', 'pid': 333} # get the output stream(data) output = stream.out.getvalue() stream.out.close() expected = self.color_start(stream.color_code) expected += stream.now().strftime(stream.time_format) + " " expected += "[333] | " + data['data'] + self.color_end() return output, expected def test_random_colored_output(self): stream = self.get_stream() output, expected = self.get_output(stream) self.assertEqual(output, expected) def test_red_colored_output(self): stream = self.get_stream(color='red') output, expected = self.get_output(stream) self.assertEqual(output, expected) def test_time_formatting(self): stream = self.get_stream(time_format='%Y/%m/%d %H.%M.%S') output, expected = self.get_output(stream) self.assertEqual(output, expected) def test_data_split_into_lines(self): stream = self.get_stream(color='red') data = {'data': '\n'.join(['foo', 'bar', 'baz']), 'pid': 333} stream(data) output = stream.out.getvalue() stream.out.close() # NOTE: We expect 4 b/c the last line needs to add a newline # in order to prepare for the next chunk self.assertEqual(len(output.split('\n')), 4) def test_data_with_extra_lines(self): stream = self.get_stream(color='red') # There is an extra newline data = {'data': '\n'.join(['foo', 'bar', 'baz', '']), 'pid': 333} stream(data) output = stream.out.getvalue() stream.out.close() self.assertEqual(len(output.split('\n')), 4) def test_color_selections(self): # The colors are chosen from an ordered list where each index # is used to calculate the ascii escape sequence. for i, color in enumerate(FancyStdoutStream.colors): stream = self.get_stream(color) self.assertEqual(i + 1, stream.color_code) stream.out.close() class TestFileStream(TestCase): stream_class = FileStream def get_stream(self, *args, **kw): # need a constant timestamp now = datetime.now() stream = self.stream_class(*args, **kw) # patch some details that will be used stream._file.close() stream._file = StringIO() stream._open = lambda: stream._file stream.now = lambda: now return stream def get_output(self, stream): # stub data data = {'data': 'hello world', 'pid': 333} # get the output stream(data) output = stream._file.getvalue() stream._file.close() expected = stream.now().strftime(stream._time_format) + " " expected += "[333] | " + data['data'] + '\n' return output, expected @skipIf(IS_WINDOWS and sys.version_info[0] < 3, "StringIO has no fileno on Python 2 and Windows") def test_time_formatting(self): stream = self.get_stream(time_format='%Y/%m/%d %H.%M.%S') output, expected = self.get_output(stream) self.assertEqual(output, expected) @skipIf(IS_WINDOWS and sys.version_info[0] < 3, "StringIO has no fileno on Python 2 and Windows") def test_data_split_into_lines(self): stream = self.get_stream(time_format='%Y/%m/%d %H.%M.%S') data = {'data': '\n'.join(['foo', 'bar', 'baz']), 'pid': 333} stream(data) output = stream._file.getvalue() stream._file.close() # NOTE: We expect 4 b/c the last line needs to add a newline # in order to prepare for the next chunk self.assertEqual(len(output.split('\n')), 4) @skipIf(IS_WINDOWS and sys.version_info[0] < 3, "StringIO has no fileno on Python 2 and Windows") def test_data_with_extra_lines(self): stream = self.get_stream(time_format='%Y/%m/%d %H.%M.%S') # There is an extra newline data = {'data': '\n'.join(['foo', 'bar', 'baz', '']), 'pid': 333} stream(data) output = stream._file.getvalue() stream._file.close() self.assertEqual(len(output.split('\n')), 4) @skipIf(IS_WINDOWS and sys.version_info[0] < 3, "StringIO has no fileno on Python 2 and Windows") def test_data_with_no_EOL(self): stream = self.get_stream() # data with no newline and more than 1024 chars data = {'data': '*' * 1100, 'pid': 333} stream(data) stream(data) output = stream._file.getvalue() stream._file.close() self.assertEqual(output, '*' * 2200) class TestWatchedFileStream(TestFileStream): stream_class = WatchedFileStream def get_real_stream(self, *args, **kw): # need a constant timestamp now = datetime.now() stream = self.stream_class(*args, **kw) stream.now = lambda: now return stream # we can't run this test on Windows due to file locking @skipIf(IS_WINDOWS, "On Windows") def test_move_file(self): _test_fd, test_filename = tempfile.mkstemp() stream = self.get_real_stream(filename=test_filename) line1_contents = 'line 1' line2_contents = 'line 2' file1 = test_filename + '.1' # write data, then move the file to simulate a log rotater that will # rename the file underneath us, then write more data to ensure that # logging continues to work after the rename stream({'data': line1_contents}) os.rename(test_filename, file1) stream({'data': line2_contents}) stream.close() with open(test_filename) as line2: self.assertEqual(line2.read().strip(), line2_contents) with open(file1) as line1: self.assertEqual(line1.read().strip(), line1_contents) os.unlink(test_filename) os.unlink(file1) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_umask.py000066400000000000000000000101271256046442300202550ustar00rootroot00000000000000import sys import os import tornado import signal import time from circus.tests.support import TestCircus, EasyTestSuite, TimeoutException from circus.tests.support import skipIf, IS_WINDOWS from circus.stream import QueueStream, Empty from circus.util import tornado_sleep from zmq.utils.strtypes import u class Process(object): def __init__(self, test_file): with open('/tmp/aack.txt', 'a') as f: if os.path.exists(test_file): f.write('Removing {0}\n'.format(test_file)) if os.path.isdir(test_file): os.removedirs(test_file) else: os.unlink(test_file) else: f.write('Hmm, {0} is missing\n'.format(test_file)) f.write('Creating folder {0}\n'.format(test_file)) os.makedirs(test_file) # init signal handling signal.signal(signal.SIGQUIT, self.handle_quit) signal.signal(signal.SIGTERM, self.handle_quit) signal.signal(signal.SIGINT, self.handle_quit) self.alive = True sys.stdout.write('Done') sys.stdout.flush() # noinspection PyUnusedLocal def handle_quit(self, *args): self.alive = False def run(self): while self.alive: time.sleep(0.1) def run_process(test_file): process = Process(test_file) process.run() return 1 @tornado.gen.coroutine def read_from_stream(stream, timeout=5): start = time.time() while time.time() - start < timeout: try: data = stream.get_nowait() raise tornado.gen.Return(u(data['data'])) except Empty: yield tornado_sleep(0.1) raise TimeoutException('Timeout reading queue') class UmaskTest(TestCircus): def setUp(self): super(UmaskTest, self).setUp() self.original_umask = os.umask(int('022', 8)) def tearDown(self): super(UmaskTest, self).tearDown() dirname = self.test_file if os.path.isdir(dirname): os.removedirs(dirname) os.umask(self.original_umask) @tornado.gen.coroutine def _call(self, _cmd, **props): resp = yield self.call(_cmd, waiting=True, **props) raise tornado.gen.Return(resp) @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_inherited(self): cmd = 'circus.tests.test_umask.run_process' stream = QueueStream() stdout_stream = {'stream': stream} yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream) res = yield read_from_stream(stream) self.assertEqual(res, 'Done') yield self.stop_arbiter() self.assertTrue(os.path.isdir(self.test_file)) mode = oct(os.stat(self.test_file).st_mode)[-3:] self.assertEqual(mode, '755') @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_set_before_launch(self): os.umask(2) cmd = 'circus.tests.test_umask.run_process' stream = QueueStream() stdout_stream = {'stream': stream} yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream) res = yield read_from_stream(stream) self.assertEqual(res, 'Done') yield self.stop_arbiter() self.assertTrue(os.path.isdir(self.test_file)) mode = oct(os.stat(self.test_file).st_mode)[-3:] self.assertEqual(mode, '775') @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_set_by_arbiter(self): cmd = 'circus.tests.test_umask.run_process' stream = QueueStream() stdout_stream = {'stream': stream} yield self.start_arbiter(cmd=cmd, stdout_stream=stdout_stream, arbiter_kw={'umask': 0}) res = yield read_from_stream(stream) self.assertEqual(res, 'Done') yield self.stop_arbiter() self.assertTrue(os.path.isdir(self.test_file)) mode = oct(os.stat(self.test_file).st_mode)[-3:] self.assertEqual(mode, '777') test_suite = EasyTestSuite(__name__) if __name__ == '__main__': run_process(sys.argv[1]) circus-0.12.1/circus/tests/test_util.py000066400000000000000000000252261256046442300201200ustar00rootroot00000000000000from __future__ import unicode_literals import tempfile import shutil import os import sys try: import grp import pwd except ImportError: grp = None pwd = None import psutil from psutil import Popen import mock from circus.tests.support import (TestCase, EasyTestSuite, skipIf, IS_WINDOWS, SLEEP) from circus import util from circus.util import ( get_info, bytes2human, human2bytes, to_bool, parse_env_str, env_to_str, to_uid, to_gid, replace_gnu_args, get_python_version, load_virtualenv, get_working_dir ) class TestUtil(TestCase): def setUp(self): self.dirs = [] def tearDown(self): for dir in self.dirs: if os.path.exists(dir): shutil.rmtree(dir) def test_get_info(self): worker = Popen(["python", "-c", SLEEP % 5]) try: info = get_info(worker) finally: worker.terminate() self.assertTrue(isinstance(info['pid'], int)) if IS_WINDOWS: self.assertEqual(info['nice'], psutil.NORMAL_PRIORITY_CLASS) else: self.assertEqual(info['nice'], 0) def test_get_info_still_works_when_denied_access(self): def access_denied(): return mock.MagicMock(side_effect=util.AccessDenied) class WorkerMock(mock.MagicMock): def __getattr__(self, attr): raise util.AccessDenied() worker = WorkerMock() worker.get_memory_info = access_denied() worker.get_cpu_percent = access_denied() worker.get_cpu_times = access_denied() worker.get_nice = access_denied() worker.get_memory_percent = access_denied() worker.cmdline = [] info = get_info(worker) self.assertEqual(info['mem'], 'N/A') self.assertEqual(info['cpu'], 'N/A') self.assertEqual(info['ctime'], 'N/A') self.assertEqual(info['pid'], 'N/A') self.assertEqual(info['username'], 'N/A') self.assertEqual(info['nice'], 'N/A') self.assertEqual(info['create_time'], 'N/A') self.assertEqual(info['age'], 'N/A') worker.nice = mock.MagicMock(side_effect=util.NoSuchProcess(1234)) self.assertEqual(get_info(worker)['nice'], 'Zombie') def test_convert_opt(self): self.assertEqual(util.convert_opt('env', {'key': 'value'}), 'key=value') self.assertEqual(util.convert_opt('test', None), '') self.assertEqual(util.convert_opt('test', 1), '1') def test_bytes2human(self): self.assertEqual(bytes2human(10000), '9K') self.assertEqual(bytes2human(100001221), '95M') self.assertRaises(TypeError, bytes2human, '1') def test_human2bytes(self): self.assertEqual(human2bytes('1B'), 1) self.assertEqual(human2bytes('9K'), 9216) self.assertEqual(human2bytes('1129M'), 1183842304) self.assertEqual(human2bytes('67T'), 73667279060992) self.assertEqual(human2bytes('13P'), 14636698788954112) self.assertRaises(ValueError, human2bytes, '') self.assertRaises(ValueError, human2bytes, 'faoej') self.assertRaises(ValueError, human2bytes, '123KB') self.assertRaises(ValueError, human2bytes, '48') self.assertRaises(ValueError, human2bytes, '23V') self.assertRaises(TypeError, human2bytes, 234) def test_tobool(self): for value in ('True ', '1', 'true'): self.assertTrue(to_bool(value)) for value in ('False', '0', 'false'): self.assertFalse(to_bool(value)) for value in ('Fal', '344', ''): self.assertRaises(ValueError, to_bool, value) def test_parse_env_str(self): env = 'booo=2,test=1' parsed = parse_env_str(env) self.assertEqual(parsed, {'test': '1', 'booo': '2'}) self.assertEqual(env_to_str(parsed), env) @skipIf(not pwd, "Pwd not supported") def test_to_uid(self): with mock.patch('pwd.getpwnam') as getpw: m = mock.Mock() m.pw_uid = '1000' getpw.return_value = m uid = to_uid('user') self.assertEqual('1000', uid) uid = to_uid('user') self.assertEqual('1000', uid) @skipIf(not grp, "Grp not supported") def test_to_uidgid(self): self.assertRaises(ValueError, to_uid, 'xxxxxxx') self.assertRaises(ValueError, to_gid, 'xxxxxxx') self.assertRaises(ValueError, to_uid, -12) self.assertRaises(ValueError, to_gid, -12) self.assertRaises(TypeError, to_uid, None) self.assertRaises(TypeError, to_gid, None) @skipIf(not pwd, "Pwd not supported") def test_to_uid_str(self): with mock.patch('pwd.getpwuid') as getpwuid: uid = to_uid('1066') self.assertEqual(1066, uid) getpwuid.assert_called_with(1066) @skipIf(not grp, "Grp not supported") def test_to_gid_str(self): with mock.patch('grp.getgrgid') as getgrgid: gid = to_gid('1042') self.assertEqual(1042, gid) getgrgid.assert_called_with(1042) @skipIf(not grp, "Grp not supported") def test_negative_uid_gid(self): # OSX allows negative uid/gid and throws KeyError on a miss. On # 32-bit and 64-bit Linux, all negative values throw KeyError as do # requests for non-existent uid/gid. def int32(val): if val & 0x80000000: val += -0x100000000 return val def uid_min_max(): uids = sorted([int32(e[2]) for e in pwd.getpwall()]) uids[0] = uids[0] if uids[0] < 0 else -1 return uids[0], uids[-1] def gid_min_max(): gids = sorted([int32(e[2]) for e in grp.getgrall()]) gids[0] = gids[0] if gids[0] < 0 else -1 return gids[0], gids[-1] uid_min, uid_max = uid_min_max() gid_min, gid_max = gid_min_max() def getpwuid(pid): return pwd.getpwuid(pid) def getgrgid(gid): return grp.getgrgid(gid) self.assertRaises(KeyError, getpwuid, uid_max + 1) self.assertRaises(KeyError, getpwuid, uid_min - 1) # getgrid may raises overflow error on mac/os x, fixed in python2.7.5 # see http://bugs.python.org/issue17531 self.assertRaises((KeyError, OverflowError), getgrgid, gid_max + 1) self.assertRaises((KeyError, OverflowError), getgrgid, gid_min - 1) def test_replace_gnu_args(self): repl = replace_gnu_args self.assertEqual('dont change --fd ((circus.me)) please', repl('dont change --fd ((circus.me)) please')) self.assertEqual('dont change --fd $(circus.me) please', repl('dont change --fd $(circus.me) please')) self.assertEqual('thats an int 2', repl('thats an int $(circus.me)', me=2)) self.assertEqual('foobar', replace_gnu_args('$(circus.test)', test='foobar')) self.assertEqual('foobar', replace_gnu_args('$(circus.test)', test='foobar')) self.assertEqual('foo, foobar, baz', replace_gnu_args('foo, $(circus.test), baz', test='foobar')) self.assertEqual('foo, foobar, baz', replace_gnu_args('foo, ((circus.test)), baz', test='foobar')) self.assertEqual('foobar', replace_gnu_args('$(cir.test)', prefix='cir', test='foobar')) self.assertEqual('foobar', replace_gnu_args('((cir.test))', prefix='cir', test='foobar')) self.assertEqual('thats an int 2', repl('thats an int $(s.me)', prefix='s', me=2)) self.assertEqual('thats an int 2', repl('thats an int ((s.me))', prefix='s', me=2)) self.assertEqual('thats an int 2', repl('thats an int $(me)', prefix=None, me=2)) self.assertEqual('thats an int 2', repl('thats an int ((me))', prefix=None, me=2)) def test_get_python_version(self): py_version = get_python_version() self.assertEqual(3, len(py_version)) for x in py_version: self.assertEqual(int, type(x)) self.assertGreaterEqual(py_version[0], 2) self.assertGreaterEqual(py_version[1], 0) self.assertGreaterEqual(py_version[2], 0) def _create_dir(self): dir = tempfile.mkdtemp() self.dirs.append(dir) return dir def test_load_virtualenv(self): watcher = mock.Mock() watcher.copy_env = False # we need the copy_env flag self.assertRaises(ValueError, load_virtualenv, watcher) watcher.copy_env = True watcher.virtualenv = 'XXX' # we want virtualenv to be a directory self.assertRaises(ValueError, load_virtualenv, watcher) watcher.virtualenv = self._create_dir() # we want virtualenv directory to contain a site-packages self.assertRaises(ValueError, load_virtualenv, watcher) py_ver = sys.version.split()[0][:3] site_pkg = os.path.join(watcher.virtualenv, 'lib', 'python%s' % py_ver, 'site-packages') os.makedirs(site_pkg) watcher.env = {} load_virtualenv(watcher) self.assertEqual(site_pkg, watcher.env['PYTHONPATH']) # test with a specific python version for the virtualenv site packages py_ver = "my_python_version" site_pkg = os.path.join(watcher.virtualenv, 'lib', 'python%s' % py_ver, 'site-packages') os.makedirs(site_pkg) watcher.env = {} load_virtualenv(watcher, py_ver=py_ver) self.assertEqual(site_pkg, watcher.env['PYTHONPATH']) @mock.patch('circus.util.os.environ', {'PWD': '/path/to/pwd'}) @mock.patch('circus.util.os.getcwd', lambda: '/path/to/cwd') def test_working_dir_return_pwd_when_paths_are_equals(self): def _stat(path): stat = mock.MagicMock() stat.ino = 'path' stat.dev = 'dev' return stat _old_os_stat = util.os.stat try: util.os.stat = _stat self.assertEqual(get_working_dir(), '/path/to/pwd') finally: util.os.stat = _old_os_stat test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_validate_option.py000066400000000000000000000045761256046442300223310ustar00rootroot00000000000000from circus.tests.support import TestCase, EasyTestSuite, IS_WINDOWS from mock import patch from circus.commands.util import validate_option from circus.exc import MessageError class TestValidateOption(TestCase): def test_uidgid(self): self.assertRaises(MessageError, validate_option, 'uid', {}) validate_option('uid', 1) validate_option('uid', 'user') self.assertRaises(MessageError, validate_option, 'gid', {}) validate_option('gid', 1) validate_option('gid', 'user') @patch('warnings.warn') def test_stdout_stream(self, warn): self.assertRaises( MessageError, validate_option, 'stdout_stream', 'something') self.assertRaises(MessageError, validate_option, 'stdout_stream', {}) validate_option('stdout_stream', {'class': 'MyClass'}) validate_option( 'stdout_stream', {'class': 'MyClass', 'my_option': '1'}) validate_option( 'stdout_stream', {'class': 'MyClass', 'refresh_time': 1}) self.assertEqual(warn.call_count, 1) @patch('warnings.warn') def test_stderr_stream(self, warn): self.assertRaises( MessageError, validate_option, 'stderr_stream', 'something') self.assertRaises(MessageError, validate_option, 'stderr_stream', {}) validate_option('stderr_stream', {'class': 'MyClass'}) validate_option( 'stderr_stream', {'class': 'MyClass', 'my_option': '1'}) validate_option( 'stderr_stream', {'class': 'MyClass', 'refresh_time': 1}) self.assertEqual(warn.call_count, 1) def test_hooks(self): validate_option('hooks', {'before_start': ['all', False]}) # make sure we control the hook names self.assertRaises(MessageError, validate_option, 'hooks', {'IDONTEXIST': ['all', False]}) def test_rlimit(self): if IS_WINDOWS: # rlimits are not supported on Windows self.assertRaises(MessageError, validate_option, 'rlimit_core', 1) else: validate_option('rlimit_core', 1) # require int parameter self.assertRaises(MessageError, validate_option, 'rlimit_core', '1') # require valid rlimit settings self.assertRaises(MessageError, validate_option, 'rlimit_foo', 1) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/test_watcher.py000066400000000000000000000536461256046442300206070ustar00rootroot00000000000000import signal import sys import os import time import warnings try: import queue as Queue except ImportError: import Queue # NOQA try: from test.support import captured_output except ImportError: try: from test.test_support import captured_output # NOQA except ImportError: captured_output = None # NOQA import tornado import mock from circus import logger from circus.process import RUNNING, UNEXISTING from circus.stream import QueueStream from circus.tests.support import TestCircus, truncate_file from circus.tests.support import async_poll_for, EasyTestSuite from circus.tests.support import MagicMockFuture, skipIf, IS_WINDOWS from circus.tests.support import PYTHON from circus.util import get_python_version, tornado_sleep from circus.watcher import Watcher from circus.py3compat import s if hasattr(signal, 'SIGKILL'): SIGKILL = signal.SIGKILL else: SIGKILL = signal.SIGTERM warnings.filterwarnings('ignore', module='threading', message='sys.exc_clear') class FakeProcess(object): def __init__(self, pid, status, started=1, age=1): self.status = status self.pid = pid self.started = started self.age = age self.stopping = False def returncode(self): return 0 def children(self): return [] def is_alive(self): return True def stop(self): pass def wait(self, *args, **kwargs): pass class TestWatcher(TestCircus): runner = None @tornado.testing.gen_test def test_decr_too_much(self): yield self.start_arbiter() res = yield self.numprocesses('decr', name='test', nb=100) self.assertEqual(res, 0) res = yield self.numprocesses('decr', name='test', nb=100) self.assertEqual(res, 0) res = yield self.numprocesses('incr', name='test', nb=1) self.assertEqual(res, 1) yield self.stop_arbiter() @tornado.testing.gen_test def test_signal(self): yield self.start_arbiter(check_delay=1.0) resp = yield self.numprocesses('incr', name='test') self.assertEqual(resp, 2) # wait for both to have started resp = yield async_poll_for(self.test_file, 'STARTSTART') self.assertTrue(resp) truncate_file(self.test_file) pids = yield self.pids() self.assertEqual(len(pids), 2) to_kill = pids[0] status = yield self.status('signal', name='test', pid=to_kill, signum=SIGKILL) self.assertEqual(status, 'ok') # make sure the process is restarted res = yield async_poll_for(self.test_file, 'START') self.assertTrue(res) # we still should have two processes, but not the same pids for them pids = yield self.pids() count = 0 while len(pids) < 2 and count < 10: pids = yield self.pids() time.sleep(.1) self.assertEqual(len(pids), 2) self.assertTrue(to_kill not in pids) yield self.stop_arbiter() @tornado.testing.gen_test def test_unexisting(self): yield self.start_arbiter() watcher = self.arbiter.get_watcher("test") to_kill = [] nb_proc = len(watcher.processes) for process in list(watcher.processes.values()): to_kill.append(process.pid) # the process is killed in an unsual way process.stop() # and wait for it to die process.wait(3) # ensure the old process is considered "unexisting" self.assertEqual(process.status, UNEXISTING) # this should clean up and create a new process yield watcher.reap_and_manage_processes() # watcher ids should have been reused wids = [p.wid for p in watcher.processes.values()] self.assertEqual(max(wids), watcher.numprocesses) self.assertEqual(sum(wids), sum(range(1, watcher.numprocesses + 1))) # we should have a new process here now self.assertEqual(len(watcher.processes), nb_proc) for p in watcher.processes.values(): # and that one needs to have a new pid. self.assertFalse(p.pid in to_kill) # and should not be unexisting... self.assertNotEqual(p.status, UNEXISTING) yield self.stop_arbiter() @tornado.testing.gen_test def test_stats(self): yield self.start_arbiter() resp = yield self.call("stats") self.assertTrue("test" in resp.get('infos')) watchers = resp.get('infos')['test'] self.assertEqual(watchers[list(watchers.keys())[0]]['cmdline'].lower(), PYTHON.split(os.sep)[-1].lower()) yield self.stop_arbiter() @tornado.testing.gen_test def test_max_age(self): yield self.start_arbiter() # let's run 15 processes yield self.numprocesses('incr', name='test', nb=14) initial_pids = yield self.pids() # we want to make sure the watcher is really up and running 14 # processes, and stable async_poll_for(self.test_file, 'START' * 15) truncate_file(self.test_file) # make sure we have a clean slate # we want a max age of 1 sec. options = {'max_age': 1, 'max_age_variance': 0} result = yield self.call('set', name='test', waiting=True, options=options) self.assertEqual(result.get('status'), 'ok') current_pids = yield self.pids() self.assertEqual(len(current_pids), 15) self.assertNotEqual(initial_pids, current_pids) yield self.stop_arbiter() @tornado.testing.gen_test def test_arbiter_reference(self): yield self.start_arbiter() self.assertEqual(self.arbiter.watchers[0].arbiter, self.arbiter) yield self.stop_arbiter() class TestWatcherInitialization(TestCircus): @tornado.testing.gen_test def test_copy_env(self): old_environ = os.environ try: os.environ = {'COCONUTS': 'MIGRATE'} watcher = Watcher("foo", "foobar", copy_env=True) self.assertEqual(watcher.env, os.environ) watcher = Watcher("foo", "foobar", copy_env=True, env={"AWESOMENESS": "YES"}) self.assertEqual(watcher.env, {'COCONUTS': 'MIGRATE', 'AWESOMENESS': 'YES'}) finally: os.environ = old_environ @tornado.testing.gen_test def test_hook_in_PYTHON_PATH(self): # we have a hook in PYTHONPATH tempdir = self.get_tmpdir() hook = 'def hook(*args, **kw):\n return True\n' with open(os.path.join(tempdir, 'plugins.py'), 'w') as f: f.write(hook) old_environ = os.environ try: os.environ = {'PYTHONPATH': tempdir} hooks = {'before_start': ('plugins.hook', False)} watcher = Watcher("foo", "foobar", copy_env=True, hooks=hooks) self.assertEqual(watcher.env, os.environ) finally: os.environ = old_environ @skipIf(IS_WINDOWS, "Streams not supported") @tornado.testing.gen_test def test_copy_path(self): watcher = SomeWatcher(stream=True) yield watcher.run() # wait for watcher data at most 5s messages = [] resp = False start_time = time.time() while (time.time() - start_time) <= 5: yield tornado_sleep(0.5) # More than one Queue.get call is needed to get full # output from a watcher in an environment with rich sys.path. try: m = watcher.stream.get(block=False) messages.append(m) except Queue.Empty: pass data = ''.join(s(m['data']) for m in messages) if 'XYZ' in data: resp = True break self.assertTrue(resp) yield watcher.stop() @skipIf(IS_WINDOWS, "virtualenv not supported yet on Windows") @tornado.testing.gen_test def test_venv(self): venv = os.path.join(os.path.dirname(__file__), 'venv') watcher = SomeWatcher(virtualenv=venv) yield watcher.run() try: py_version = get_python_version() major = py_version[0] minor = py_version[1] wanted = os.path.join(venv, 'lib', 'python%d.%d' % (major, minor), 'site-packages', 'pip-7.7-py%d.%d.egg' % (major, minor)) ppath = watcher.watcher.env['PYTHONPATH'] finally: yield watcher.stop() self.assertTrue(wanted in ppath) @skipIf(IS_WINDOWS, "virtualenv not supported yet on Windows") @tornado.testing.gen_test def test_venv_site_packages(self): venv = os.path.join(os.path.dirname(__file__), 'venv') watcher = SomeWatcher(virtualenv=venv) yield watcher.run() try: yield tornado_sleep(1) py_version = get_python_version() major = py_version[0] minor = py_version[1] wanted = os.path.join(venv, 'lib', 'python%d.%d' % (major, minor), 'site-packages') ppath = watcher.watcher.env['PYTHONPATH'] finally: yield watcher.stop() self.assertTrue(wanted in ppath.split(os.pathsep)) @skipIf(IS_WINDOWS, "virtualenv not supported yet on Windows") @tornado.testing.gen_test def test_venv_py_ver(self): py_ver = "my_py_ver" venv = os.path.join(os.path.dirname(__file__), 'venv') wanted = os.path.join(venv, 'lib', 'python%s' % py_ver, 'site-packages') if not os.path.exists(wanted): os.makedirs(wanted) watcher = SomeWatcher(virtualenv=venv, virtualenv_py_ver=py_ver) yield watcher.run() try: yield tornado_sleep(1) ppath = watcher.watcher.env['PYTHONPATH'] finally: yield watcher.stop() self.assertTrue(wanted in ppath.split(os.pathsep)) class SomeWatcher(object): def __init__(self, stream=False, loop=None, **kw): if stream: self.stream = QueueStream() else: self.stream = None self.watcher = None self.kw = kw if loop is None: self.loop = tornado.ioloop.IOLoop.instance() else: self.loop = loop @tornado.gen.coroutine def run(self): if self.stream: qstream = {'stream': self.stream} else: qstream = None old_environ = os.environ old_paths = sys.path[:] try: sys.path = ['XYZ'] os.environ = {'COCONUTS': 'MIGRATE'} cmd = ('%s -c "import sys; ' 'sys.stdout.write(\':\'.join(sys.path)); ' ' sys.stdout.flush()"') % PYTHON self.watcher = Watcher('xx', cmd, copy_env=True, copy_path=True, stdout_stream=qstream, loop=self.loop, **self.kw) yield self.watcher.start() finally: os.environ = old_environ sys.path[:] = old_paths @tornado.gen.coroutine def stop(self): if self.watcher is not None: yield self.watcher.stop() SUCCESS = 1 FAILURE = 2 ERROR = 3 class TestWatcherHooks(TestCircus): def run_with_hooks(self, hooks, streams=False): if streams: self.stream = QueueStream() self.errstream = QueueStream() stdout_stream = {'stream': self.stream} stderr_stream = {'stream': self.errstream} else: self.stream = None self.errstream = None stdout_stream = None stderr_stream = None dummy_process = 'circus.tests.support.run_process' return self._create_circus(dummy_process, stdout_stream=stdout_stream, stderr_stream=stderr_stream, hooks=hooks, debug=True, async=True) @tornado.gen.coroutine def _stop(self): yield self.call("stop", name="test", waiting=True) @tornado.gen.coroutine def _stats(self): yield self.call("stats", name="test") @tornado.gen.coroutine def _extended_stats(self): yield self.call("stats", name="test", extended=True) @tornado.gen.coroutine def get_status(self): resp = yield self.call("status", name="test") raise tornado.gen.Return(resp['status']) def test_missing_hook(self): hooks = {'before_start': ('fake.hook.path', False)} self.assertRaises(ImportError, self.run_with_hooks, hooks) @tornado.gen.coroutine def _test_hooks(self, hook_name='before_start', status='active', behavior=SUCCESS, call=None, hook_kwargs_test_function=None): events = {'before_start_called': False} def hook(watcher, arbiter, hook_name, **kwargs): events['%s_called' % hook_name] = True events['arbiter_in_hook'] = arbiter if hook_kwargs_test_function is not None: hook_kwargs_test_function(kwargs) if hook_name == 'extended_stats': kwargs['stats']['tx'] = 1000 return if behavior == SUCCESS: return True elif behavior == FAILURE: return False raise TypeError('beeeuuua') old = logger.exception logger.exception = lambda x: x hooks = {hook_name: (hook, False)} testfile, arbiter = self.run_with_hooks(hooks) yield arbiter.start() try: if call: yield call() resp_status = yield self.get_status() self.assertEqual(resp_status, status) finally: yield arbiter.stop() logger.exception = old self.assertTrue(events['%s_called' % hook_name]) self.assertEqual(events['arbiter_in_hook'], arbiter) @tornado.gen.coroutine def _test_extended_stats(self, extended=False): events = {'extended_stats_called': False} def hook(watcher, arbiter, hook_name, **kwargs): events['extended_stats_called'] = True old = logger.exception logger.exception = lambda x: x hooks = {'extended_stats': (hook, False)} testfile, arbiter = self.run_with_hooks(hooks) yield arbiter.start() try: if extended: yield self._extended_stats() else: yield self._stats() resp_status = yield self.get_status() self.assertEqual(resp_status, 'active') finally: yield arbiter.stop() logger.exception = old self.assertEqual(events['extended_stats_called'], extended) @tornado.testing.gen_test def test_before_start(self): yield self._test_hooks() @tornado.testing.gen_test def test_before_start_fails(self): yield self._test_hooks(behavior=ERROR, status='stopped') @tornado.testing.gen_test def test_before_start_false(self): yield self._test_hooks(behavior=FAILURE, status='stopped', hook_name='after_start') @tornado.testing.gen_test def test_after_start(self): yield self._test_hooks(hook_name='after_start') @tornado.testing.gen_test def test_after_start_fails(self): if captured_output: with captured_output('stderr'): yield self._test_hooks(behavior=ERROR, status='stopped', hook_name='after_start') @tornado.testing.gen_test def test_after_start_false(self): yield self._test_hooks(behavior=FAILURE, status='stopped', hook_name='after_start') @tornado.testing.gen_test def test_before_stop(self): yield self._test_hooks(hook_name='before_stop', status='stopped', call=self._stop) def _hook_signal_kwargs_test_function(self, kwargs): self.assertTrue("pid" not in kwargs) self.assertTrue("signum" not in kwargs) self.assertTrue(kwargs["pid"] in (signal.SIGTERM, SIGKILL)) self.assertTrue(int(kwargs["signum"]) > 1) @tornado.testing.gen_test def test_before_signal(self): func = self._hook_signal_kwargs_test_function yield self._test_hooks(hook_name='before_signal', status='stopped', call=self._stop, hook_kwargs_test_function=func) @tornado.testing.gen_test def test_after_signal(self): func = self._hook_signal_kwargs_test_function yield self._test_hooks(hook_name='after_signal', status='stopped', call=self._stop, hook_kwargs_test_function=func) @tornado.testing.gen_test def test_before_stop_fails(self): if captured_output: with captured_output('stdout'): yield self._test_hooks(behavior=ERROR, status='stopped', hook_name='before_stop', call=self._stop) @tornado.testing.gen_test def test_before_stop_false(self): yield self._test_hooks(behavior=FAILURE, status='stopped', hook_name='before_stop', call=self._stop) @tornado.testing.gen_test def test_after_stop(self): yield self._test_hooks(hook_name='after_stop', status='stopped', call=self._stop) @tornado.testing.gen_test def test_after_stop_fails(self): if captured_output: with captured_output('stdout'): yield self._test_hooks(behavior=ERROR, status='stopped', hook_name='after_stop', call=self._stop) @tornado.testing.gen_test def test_after_stop_false(self): yield self._test_hooks(behavior=FAILURE, status='stopped', hook_name='after_stop', call=self._stop) @tornado.testing.gen_test def test_before_spawn(self): yield self._test_hooks(hook_name='before_spawn') @tornado.testing.gen_test def test_before_spawn_failure(self): if captured_output: with captured_output('stdout'): yield self._test_hooks(behavior=ERROR, status='stopped', hook_name='before_spawn', call=self._stop) @tornado.testing.gen_test def test_before_spawn_false(self): yield self._test_hooks(behavior=FAILURE, status='stopped', hook_name='before_spawn', call=self._stop) @tornado.testing.gen_test def test_after_spawn(self): yield self._test_hooks(hook_name='after_spawn') @tornado.testing.gen_test def test_after_spawn_failure(self): if captured_output: with captured_output('stdout'): yield self._test_hooks(behavior=ERROR, status='stopped', hook_name='after_spawn', call=self._stop) @tornado.testing.gen_test def test_after_spawn_false(self): yield self._test_hooks(behavior=FAILURE, status='stopped', hook_name='after_spawn', call=self._stop) @tornado.testing.gen_test def test_extended_stats(self): yield self._test_extended_stats() yield self._test_extended_stats(extended=True) def oneshot_process(test_file): pass class RespawnTest(TestCircus): @tornado.testing.gen_test def test_not_respawning(self): oneshot_process = 'circus.tests.test_watcher.oneshot_process' testfile, arbiter = self._create_circus(oneshot_process, respawn=False, async=True) yield arbiter.start() watcher = arbiter.watchers[-1] try: # Per default, we shouldn't respawn processes, # so we should have one process, even if in a dead state. resp = yield self.call("numprocesses", name="test") self.assertEqual(resp['numprocesses'], 1) # let's reap processes and explicitely ask for process management yield watcher.reap_and_manage_processes() # we should have zero processes (the process shouldn't respawn) self.assertEqual(len(watcher.processes), 0) # If we explicitely ask the watcher to respawn its processes, # ensure it's doing so. yield watcher.start() self.assertEqual(len(watcher.processes), 1) finally: yield arbiter.stop() @tornado.testing.gen_test def test_stopping_a_watcher_doesnt_spawn(self): watcher = Watcher("foo", "foobar", respawn=True, numprocesses=3, graceful_timeout=0) watcher._status = "started" watcher.spawn_processes = MagicMockFuture() watcher.send_signal = mock.MagicMock() # We have one running process and a dead one. watcher.processes = {1234: FakeProcess(1234, status=RUNNING), 1235: FakeProcess(1235, status=RUNNING)} # When we call manage_process(), the watcher should try to spawn a new # process since we aim to have 3 of them. yield watcher.manage_processes() self.assertTrue(watcher.spawn_processes.called) # Now, we want to stop everything. watcher.processes = {1234: FakeProcess(1234, status=RUNNING), 1235: FakeProcess(1235, status=RUNNING)} watcher.spawn_processes.reset_mock() yield watcher.stop() yield watcher.manage_processes() # And be sure we don't spawn new processes in the meantime. self.assertFalse(watcher.spawn_processes.called) test_suite = EasyTestSuite(__name__) circus-0.12.1/circus/tests/venv/000077500000000000000000000000001256046442300165015ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/000077500000000000000000000000001256046442300172475ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/000077500000000000000000000000001256046442300210165ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/no-global-site-packages.txt000066400000000000000000000000001256046442300261350ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/orig-prefix.txt000066400000000000000000000000701256046442300240070ustar00rootroot00000000000000/System/Library/Frameworks/Python.framework/Versions/2.6circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/000077500000000000000000000000001256046442300235365ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/easy-install.pth000066400000000000000000000003151256046442300266570ustar00rootroot00000000000000import sys; sys.__plen = len(sys.path) ./pip-7.7-py2.6.egg import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/000077500000000000000000000000001256046442300262545ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO/000077500000000000000000000000001256046442300274075ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO/PKG-INFO000066400000000000000000000624131256046442300305120ustar00rootroot00000000000000Metadata-Version: 1.0 Name: pip Version: 1.1 Summary: pip installs packages. Python packages. An easy_install replacement Home-page: http://www.pip-installer.org Author: The pip developers Author-email: python-virtualenv@groups.google.com License: MIT Description: pip === `pip` is a tool for installing and managing Python packages, such as those found in the `Python Package Index`_. It's a replacement for easy_install_. :: $ pip install simplejson [... progress report ...] Successfully installed simplejson .. _`Python Package Index`: http://pypi.python.org/pypi .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall Upgrading a package:: $ pip install --upgrade simplejson [... progress report ...] Successfully installed simplejson Removing a package:: $ pip uninstall simplejson Uninstalling simplejson: /home/me/env/lib/python2.7/site-packages/simplejson /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info Proceed (y/n)? y Successfully uninstalled simplejson .. comment: The main website for pip is `www.pip-installer.org `_. You can also install the `in-development version `_ of pip with ``easy_install pip==dev``. Community --------- The homepage for pip is at `pip-installer.org `_. Bugs can be filed in the `pip issue tracker `_. Discussion happens on the `virtualenv email group `_. ==== News ==== Changelog ========= Next release (1.2) schedule --------------------------- Beta and final releases planned for the second half of 2012. 1.1 (2012-02-16) ---------------- * Fixed issue #326 - don't crash when a package's setup.py emits UTF-8 and then fails. Thanks Marc Abramowitz. * Added ``--target`` option for installing directly to arbitrary directory. Thanks Stavros Korokithakis. * Added support for authentication with Subversion repositories. Thanks Qiangning Hong. * Fixed issue #315 - ``--download`` now downloads dependencies as well. Thanks Qiangning Hong. * Errors from subprocesses will display the current working directory. Thanks Antti Kaihola. * Fixed issue #369 - compatibility with Subversion 1.7. Thanks Qiangning Hong. Note that setuptools remains incompatible with Subversion 1.7; to get the benefits of pip's support you must use Distribute rather than setuptools. * Fixed issue #57 - ignore py2app-generated OS X mpkg zip files in finder. Thanks Rene Dudfield. * Fixed issue #182 - log to ~/Library/Logs/ by default on OS X framework installs. Thanks Dan Callahan for report and patch. * Fixed issue #310 - understand version tags without minor version ("py3") in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for patch. * Fixed issue #7 - Pip now supports optionally installing setuptools "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker and Olivier Girardot. * Fixed issue #391 - freeze no longer borks on requirements files with --index-url or --find-links. Thanks Herbert Pfennig. * Fixed issue #288 - handle symlinks properly. Thanks lebedov for the patch. * Fixed issue #49 - pip install -U no longer reinstalls the same versions of packages. Thanks iguananaut for the pull request. * Removed ``-E`` option and ``PIP_RESPECT_VIRTUALENV``; both use a restart-in-venv mechanism that's broken, and neither one is useful since every virtualenv now has pip inside it. * Fixed issue #366 - pip throws IndexError when it calls `scraped_rel_links` * Fixed issue #22 - pip search should set and return a userful shell status code * Fixed issue #351 and #365 - added global ``--exists-action`` command line option to easier script file exists conflicts, e.g. from editable requirements from VCS that have a changed repo URL. 1.0.2 (2011-07-16) ------------------ * Fixed docs issues. * Fixed issue #295 - Reinstall a package when using the ``install -I`` option * Fixed issue #283 - Finds a Git tag pointing to same commit as origin/master * Fixed issue #279 - Use absolute path for path to docs in setup.py * Fixed issue #314 - Correctly handle exceptions on Python3. * Fixed issue #320 - Correctly parse ``--editable`` lines in requirements files 1.0.1 (2011-04-30) ------------------ * Start to use git-flow. * Fixed issue #274 - `find_command` should not raise AttributeError * Fixed issue #273 - respect Content-Disposition header. Thanks Bradley Ayers. * Fixed issue #233 - pathext handling on Windows. * Fixed issue #252 - svn+svn protocol. * Fixed issue #44 - multiple CLI searches. * Fixed issue #266 - current working directory when running setup.py clean. 1.0 (2011-04-04) ---------------- * Added Python 3 support! Huge thanks to Vinay Sajip, Vitaly Babiy, Kelsey Hightower, and Alex Gronholm, among others. * Download progress only shown on a real TTY. Thanks Alex Morega. * Fixed finding of VCS binaries to not be fooled by same-named directories. Thanks Alex Morega. * Fixed uninstall of packages from system Python for users of Debian/Ubuntu python-setuptools package (workaround until fixed in Debian and Ubuntu). * Added `get-pip.py `_ installer. Simply download and execute it, using the Python interpreter of your choice:: $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py $ python get-pip.py This may have to be run as root. .. note:: Make sure you have `distribute `_ installed before using the installer! 0.8.3 ----- * Moved main repository to Github: https://github.com/pypa/pip * Transferred primary maintenance from Ian to Jannis Leidel, Carl Meyer, Brian Rosner * Fixed issue #14 - No uninstall-on-upgrade with URL package. Thanks Oliver Tonnhofer * Fixed issue #163 - Egg name not properly resolved. Thanks Igor Sobreira * Fixed issue #178 - Non-alphabetical installation of requirements. Thanks Igor Sobreira * Fixed issue #199 - Documentation mentions --index instead of --index-url. Thanks Kelsey Hightower * Fixed issue #204 - rmtree undefined in mercurial.py. Thanks Kelsey Hightower * Fixed bug in Git vcs backend that would break during reinstallation. * Fixed bug in Mercurial vcs backend related to pip freeze and branch/tag resolution. * Fixed bug in version string parsing related to the suffix "-dev". 0.8.2 ----- * Avoid redundant unpacking of bundles (from pwaller) * Fixed issue #32, #150, #161 - Fixed checking out the correct tag/branch/commit when updating an editable Git requirement. * Fixed issue #49 - Added ability to install version control requirements without making them editable, e.g.:: pip install git+https://github.com/pypa/pip/ * Fixed issue #175 - Correctly locate build and source directory on Mac OS X. * Added ``git+https://`` scheme to Git VCS backend. 0.8.1 ----- * Added global --user flag as shortcut for --install-option="--user". From Ronny Pfannschmidt. * Added support for `PyPI mirrors `_ as defined in `PEP 381 `_, from Jannis Leidel. * Fixed issue #138 - Git revisions ignored. Thanks John-Scott Atlakson. * Fixed issue #95 - Initial editable install of github package from a tag fails. Thanks John-Scott Atlakson. * Fixed issue #107 - Can't install if a directory in cwd has the same name as the package you're installing. * Fixed issue #39 - --install-option="--prefix=~/.local" ignored with -e. Thanks Ronny Pfannschmidt and Wil Tan. 0.8 --- * Track which ``build/`` directories pip creates, never remove directories it doesn't create. From Hugo Lopes Tavares. * Pip now accepts file:// index URLs. Thanks Dave Abrahams. * Various cleanup to make test-running more consistent and less fragile. Thanks Dave Abrahams. * Real Windows support (with passing tests). Thanks Dave Abrahams. * ``pip-2.7`` etc. scripts are created (Python-version specific scripts) * ``contrib/build-standalone`` script creates a runnable ``.zip`` form of pip, from Jannis Leidel * Editable git repos are updated when reinstalled * Fix problem with ``--editable`` when multiple ``.egg-info/`` directories are found. * A number of VCS-related fixes for ``pip freeze``, from Hugo Lopes Tavares. * Significant test framework changes, from Hugo Lopes Tavares. 0.7.2 ----- * Set zip_safe=False to avoid problems some people are encountering where pip is installed as a zip file. 0.7.1 ----- * Fixed opening of logfile with no directory name. Thanks Alexandre Conrad. * Temporary files are consistently cleaned up, especially after installing bundles, also from Alex Conrad. * Tests now require at least ScriptTest 1.0.3. 0.7 --- * Fixed uninstallation on Windows * Added ``pip search`` command. * Tab-complete names of installed distributions for ``pip uninstall``. * Support tab-completion when there is a global-option before the subcommand. * Install header files in standard (scheme-default) location when installing outside a virtualenv. Install them to a slightly more consistent non-standard location inside a virtualenv (since the standard location is a non-writable symlink to the global location). * pip now logs to a central location by default (instead of creating ``pip-log.txt`` all over the place) and constantly overwrites the file in question. On Unix and Mac OS X this is ``'$HOME/.pip/pip.log'`` and on Windows it's ``'%HOME%\\pip\\pip.log'``. You are still able to override this location with the ``$PIP_LOG_FILE`` environment variable. For a complete (appended) logfile use the separate ``'--log'`` command line option. * Fixed an issue with Git that left an editable packge as a checkout of a remote branch, even if the default behaviour would have been fine, too. * Fixed installing from a Git tag with older versions of Git. * Expand "~" in logfile and download cache paths. * Speed up installing from Mercurial repositories by cloning without updating the working copy multiple times. * Fixed installing directly from directories (e.g. ``pip install path/to/dir/``). * Fixed installing editable packages with ``svn+ssh`` URLs. * Don't print unwanted debug information when running the freeze command. * Create log file directory automatically. Thanks Alexandre Conrad. * Make test suite easier to run successfully. Thanks Dave Abrahams. * Fixed "pip install ." and "pip install .."; better error for directory without setup.py. Thanks Alexandre Conrad. * Support Debian/Ubuntu "dist-packages" in zip command. Thanks duckx. * Fix relative --src folder. Thanks Simon Cross. * Handle missing VCS with an error message. Thanks Alexandre Conrad. * Added --no-download option to install; pairs with --no-install to separate download and installation into two steps. Thanks Simon Cross. * Fix uninstalling from requirements file containing -f, -i, or --extra-index-url. * Leftover build directories are now removed. Thanks Alexandre Conrad. 0.6.3 ----- * Fixed import error on Windows with regard to the backwards compatibility package 0.6.2 ----- * Fixed uninstall when /tmp is on a different filesystem. * Fixed uninstallation of distributions with namespace packages. 0.6.1 ----- * Added support for the ``https`` and ``http-static`` schemes to the Mercurial and ``ftp`` scheme to the Bazaar backend. * Fixed uninstallation of scripts installed with easy_install. * Fixed an issue in the package finder that could result in an infinite loop while looking for links. * Fixed issue with ``pip bundle`` and local files (which weren't being copied into the bundle), from Whit Morriss. 0.6 --- * Add ``pip uninstall`` and uninstall-before upgrade (from Carl Meyer). * Extended configurability with config files and environment variables. * Allow packages to be upgraded, e.g., ``pip install Package==0.1`` then ``pip install Package==0.2``. * Allow installing/upgrading to Package==dev (fix "Source version does not match target version" errors). * Added command and option completion for bash and zsh. * Extended integration with virtualenv by providing an option to automatically use an active virtualenv and an option to warn if no active virtualenv is found. * Fixed a bug with pip install --download and editable packages, where directories were being set with 0000 permissions, now defaults to 755. * Fixed uninstallation of easy_installed console_scripts. * Fixed uninstallation on Mac OS X Framework layout installs * Fixed bug preventing uninstall of editables with source outside venv. * Creates download cache directory if not existing. 0.5.1 ----- * Fixed a couple little bugs, with git and with extensions. 0.5 --- * Added ability to override the default log file name (``pip-log.txt``) with the environmental variable ``$PIP_LOG_FILE``. * Made the freeze command print installed packages to stdout instead of writing them to a file. Use simple redirection (e.g. ``pip freeze > stable-req.txt``) to get a file with requirements. * Fixed problem with freezing editable packages from a Git repository. * Added support for base URLs using ```` when parsing HTML pages. * Fixed installing of non-editable packages from version control systems. * Fixed issue with Bazaar's bzr+ssh scheme. * Added --download-dir option to the install command to retrieve package archives. If given an editable package it will create an archive of it. * Added ability to pass local file and directory paths to ``--find-links``, e.g. ``--find-links=file:///path/to/my/private/archive`` * Reduced the amount of console log messages when fetching a page to find a distribution was problematic. The full messages can be found in pip-log.txt. * Added ``--no-deps`` option to install ignore package dependencies * Added ``--no-index`` option to ignore the package index (PyPI) temporarily * Fixed installing editable packages from Git branches. * Fixes freezing of editable packages from Mercurial repositories. * Fixed handling read-only attributes of build files, e.g. of Subversion and Bazaar on Windows. * When downloading a file from a redirect, use the redirected location's extension to guess the compression (happens specifically when redirecting to a bitbucket.org tip.gz file). * Editable freeze URLs now always use revision hash/id rather than tip or branch names which could move. * Fixed comparison of repo URLs so incidental differences such as presence/absence of final slashes or quoted/unquoted special characters don't trigger "ignore/switch/wipe/backup" choice. * Fixed handling of attempt to checkout editable install to a non-empty, non-repo directory. 0.4 --- * Make ``-e`` work better with local hg repositories * Construct PyPI URLs the exact way easy_install constructs URLs (you might notice this if you use a custom index that is slash-sensitive). * Improvements on Windows (from `Ionel Maries Cristian `_). * Fixed problem with not being able to install private git repositories. * Make ``pip zip`` zip all its arguments, not just the first. * Fix some filename issues on Windows. * Allow the ``-i`` and ``--extra-index-url`` options in requirements files. * Fix the way bundle components are unpacked and moved around, to make bundles work. * Adds ``-s`` option to allow the access to the global site-packages if a virtualenv is to be created. * Fixed support for Subversion 1.6. 0.3.1 ----- * Improved virtualenv restart and various path/cleanup problems on win32. * Fixed a regression with installing from svn repositories (when not using ``-e``). * Fixes when installing editable packages that put their source in a subdirectory (like ``src/``). * Improve ``pip -h`` 0.3 --- * Added support for editable packages created from Git, Mercurial and Bazaar repositories and ability to freeze them. Refactored support for version control systems. * Do not use ``sys.exit()`` from inside the code, instead use a return. This will make it easier to invoke programmatically. * Put the install record in ``Package.egg-info/installed-files.txt`` (previously they went in ``site-packages/install-record-Package.txt``). * Fix a problem with ``pip freeze`` not including ``-e svn+`` when an svn structure is peculiar. * Allow ``pip -E`` to work with a virtualenv that uses a different version of Python than the parent environment. * Fixed Win32 virtualenv (``-E``) option. * Search the links passed in with ``-f`` for packages. * Detect zip files, even when the file doesn't have a ``.zip`` extension and it is served with the wrong Content-Type. * Installing editable from existing source now works, like ``pip install -e some/path/`` will install the package in ``some/path/``. Most importantly, anything that package requires will also be installed by pip. * Add a ``--path`` option to ``pip un/zip``, so you can avoid zipping files that are outside of where you expect. * Add ``--simulate`` option to ``pip zip``. 0.2.1 ----- * Fixed small problem that prevented using ``pip.py`` without actually installing pip. * Fixed ``--upgrade``, which would download and appear to install upgraded packages, but actually just reinstall the existing package. * Fixed Windows problem with putting the install record in the right place, and generating the ``pip`` script with Setuptools. * Download links that include embedded spaces or other unsafe characters (those characters get %-encoded). * Fixed use of URLs in requirement files, and problems with some blank lines. * Turn some tar file errors into warnings. 0.2 --- * Renamed to ``pip``, and to install you now do ``pip install PACKAGE`` * Added command ``pip zip PACKAGE`` and ``pip unzip PACKAGE``. This is particularly intended for Google App Engine to manage libraries to stay under the 1000-file limit. * Some fixes to bundles, especially editable packages and when creating a bundle using unnamed packages (like just an svn repository without ``#egg=Package``). 0.1.4 ----- * Added an option ``--install-option`` to pass options to pass arguments to ``setup.py install`` * ``.svn/`` directories are no longer included in bundles, as these directories are specific to a version of svn -- if you build a bundle on a system with svn 1.5, you can't use the checkout on a system with svn 1.4. Instead a file ``svn-checkout.txt`` is included that notes the original location and revision, and the command you can use to turn it back into an svn checkout. (Probably unpacking the bundle should, maybe optionally, recreate this information -- but that is not currently implemented, and it would require network access.) * Avoid ambiguities over project name case, where for instance MyPackage and mypackage would be considered different packages. This in particular caused problems on Macs, where ``MyPackage/`` and ``mypackage/`` are the same directory. * Added support for an environmental variable ``$PIP_DOWNLOAD_CACHE`` which will cache package downloads, so future installations won't require large downloads. Network access is still required, but just some downloads will be avoided when using this. 0.1.3 ----- * Always use ``svn checkout`` (not ``export``) so that ``tag_svn_revision`` settings give the revision of the package. * Don't update checkouts that came from ``.pybundle`` files. 0.1.2 ----- * Improve error text when there are errors fetching HTML pages when seeking packages. * Improve bundles: include empty directories, make them work with editable packages. * If you use ``-E env`` and the environment ``env/`` doesn't exist, a new virtual environment will be created. * Fix ``dependency_links`` for finding packages. 0.1.1 ----- * Fixed a NameError exception when running pip outside of a virtualenv environment. * Added HTTP proxy support (from Prabhu Ramachandran) * Fixed use of ``hashlib.md5`` on python2.5+ (also from Prabhu Ramachandran) 0.1 --- * Initial release Keywords: easy_install distutils setuptools egg virtualenv Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Build Tools Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO/SOURCES.txt000066400000000000000000000017151256046442300312770ustar00rootroot00000000000000AUTHORS.txt LICENSE.txt MANIFEST.in setup.cfg setup.py docs/ci-server-step-by-step.txt docs/configuration.txt docs/contributing.txt docs/glossary.txt docs/index.txt docs/installing.txt docs/news.txt docs/other-tools.txt docs/requirements.txt docs/usage.txt pip/__init__.py pip/_pkgutil.py pip/backwardcompat.py pip/basecommand.py pip/baseparser.py pip/download.py pip/exceptions.py pip/index.py pip/locations.py pip/log.py pip/req.py pip/runner.py pip/status_codes.py pip/util.py pip.egg-info/PKG-INFO pip.egg-info/SOURCES.txt pip.egg-info/dependency_links.txt pip.egg-info/entry_points.txt pip.egg-info/not-zip-safe pip.egg-info/top_level.txt pip/commands/__init__.py pip/commands/bundle.py pip/commands/completion.py pip/commands/freeze.py pip/commands/help.py pip/commands/install.py pip/commands/search.py pip/commands/uninstall.py pip/commands/unzip.py pip/commands/zip.py pip/vcs/__init__.py pip/vcs/bazaar.py pip/vcs/git.py pip/vcs/mercurial.py pip/vcs/subversion.pydependency_links.txt000066400000000000000000000000011256046442300333760ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO entry_points.txt000066400000000000000000000000651256046442300326270ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO[console_scripts] pip = pip:main pip-2.7 = pip:main circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO/not-zip-safe000066400000000000000000000000011256046442300316350ustar00rootroot00000000000000 circus-0.12.1/circus/tests/venv/lib/python2.6/site-packages/pip-7.7-py2.6.egg/EGG-INFO/top_level.txt000066400000000000000000000000041256046442300321330ustar00rootroot00000000000000pip circus-0.12.1/circus/tests/venv/lib/python2.7/000077500000000000000000000000001256046442300210175ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/no-global-site-packages.txt000066400000000000000000000000001256046442300261360ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/orig-prefix.txt000066400000000000000000000000701256046442300240100ustar00rootroot00000000000000/System/Library/Frameworks/Python.framework/Versions/2.7circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/000077500000000000000000000000001256046442300235375ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/easy-install.pth000066400000000000000000000003151256046442300266600ustar00rootroot00000000000000import sys; sys.__plen = len(sys.path) ./pip-7.7-py2.7.egg import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/000077500000000000000000000000001256046442300262565ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO/000077500000000000000000000000001256046442300274115ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO/PKG-INFO000066400000000000000000000624131256046442300305140ustar00rootroot00000000000000Metadata-Version: 1.0 Name: pip Version: 1.1 Summary: pip installs packages. Python packages. An easy_install replacement Home-page: http://www.pip-installer.org Author: The pip developers Author-email: python-virtualenv@groups.google.com License: MIT Description: pip === `pip` is a tool for installing and managing Python packages, such as those found in the `Python Package Index`_. It's a replacement for easy_install_. :: $ pip install simplejson [... progress report ...] Successfully installed simplejson .. _`Python Package Index`: http://pypi.python.org/pypi .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall Upgrading a package:: $ pip install --upgrade simplejson [... progress report ...] Successfully installed simplejson Removing a package:: $ pip uninstall simplejson Uninstalling simplejson: /home/me/env/lib/python2.7/site-packages/simplejson /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info Proceed (y/n)? y Successfully uninstalled simplejson .. comment: The main website for pip is `www.pip-installer.org `_. You can also install the `in-development version `_ of pip with ``easy_install pip==dev``. Community --------- The homepage for pip is at `pip-installer.org `_. Bugs can be filed in the `pip issue tracker `_. Discussion happens on the `virtualenv email group `_. ==== News ==== Changelog ========= Next release (1.2) schedule --------------------------- Beta and final releases planned for the second half of 2012. 1.1 (2012-02-16) ---------------- * Fixed issue #326 - don't crash when a package's setup.py emits UTF-8 and then fails. Thanks Marc Abramowitz. * Added ``--target`` option for installing directly to arbitrary directory. Thanks Stavros Korokithakis. * Added support for authentication with Subversion repositories. Thanks Qiangning Hong. * Fixed issue #315 - ``--download`` now downloads dependencies as well. Thanks Qiangning Hong. * Errors from subprocesses will display the current working directory. Thanks Antti Kaihola. * Fixed issue #369 - compatibility with Subversion 1.7. Thanks Qiangning Hong. Note that setuptools remains incompatible with Subversion 1.7; to get the benefits of pip's support you must use Distribute rather than setuptools. * Fixed issue #57 - ignore py2app-generated OS X mpkg zip files in finder. Thanks Rene Dudfield. * Fixed issue #182 - log to ~/Library/Logs/ by default on OS X framework installs. Thanks Dan Callahan for report and patch. * Fixed issue #310 - understand version tags without minor version ("py3") in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for patch. * Fixed issue #7 - Pip now supports optionally installing setuptools "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker and Olivier Girardot. * Fixed issue #391 - freeze no longer borks on requirements files with --index-url or --find-links. Thanks Herbert Pfennig. * Fixed issue #288 - handle symlinks properly. Thanks lebedov for the patch. * Fixed issue #49 - pip install -U no longer reinstalls the same versions of packages. Thanks iguananaut for the pull request. * Removed ``-E`` option and ``PIP_RESPECT_VIRTUALENV``; both use a restart-in-venv mechanism that's broken, and neither one is useful since every virtualenv now has pip inside it. * Fixed issue #366 - pip throws IndexError when it calls `scraped_rel_links` * Fixed issue #22 - pip search should set and return a userful shell status code * Fixed issue #351 and #365 - added global ``--exists-action`` command line option to easier script file exists conflicts, e.g. from editable requirements from VCS that have a changed repo URL. 1.0.2 (2011-07-16) ------------------ * Fixed docs issues. * Fixed issue #295 - Reinstall a package when using the ``install -I`` option * Fixed issue #283 - Finds a Git tag pointing to same commit as origin/master * Fixed issue #279 - Use absolute path for path to docs in setup.py * Fixed issue #314 - Correctly handle exceptions on Python3. * Fixed issue #320 - Correctly parse ``--editable`` lines in requirements files 1.0.1 (2011-04-30) ------------------ * Start to use git-flow. * Fixed issue #274 - `find_command` should not raise AttributeError * Fixed issue #273 - respect Content-Disposition header. Thanks Bradley Ayers. * Fixed issue #233 - pathext handling on Windows. * Fixed issue #252 - svn+svn protocol. * Fixed issue #44 - multiple CLI searches. * Fixed issue #266 - current working directory when running setup.py clean. 1.0 (2011-04-04) ---------------- * Added Python 3 support! Huge thanks to Vinay Sajip, Vitaly Babiy, Kelsey Hightower, and Alex Gronholm, among others. * Download progress only shown on a real TTY. Thanks Alex Morega. * Fixed finding of VCS binaries to not be fooled by same-named directories. Thanks Alex Morega. * Fixed uninstall of packages from system Python for users of Debian/Ubuntu python-setuptools package (workaround until fixed in Debian and Ubuntu). * Added `get-pip.py `_ installer. Simply download and execute it, using the Python interpreter of your choice:: $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py $ python get-pip.py This may have to be run as root. .. note:: Make sure you have `distribute `_ installed before using the installer! 0.8.3 ----- * Moved main repository to Github: https://github.com/pypa/pip * Transferred primary maintenance from Ian to Jannis Leidel, Carl Meyer, Brian Rosner * Fixed issue #14 - No uninstall-on-upgrade with URL package. Thanks Oliver Tonnhofer * Fixed issue #163 - Egg name not properly resolved. Thanks Igor Sobreira * Fixed issue #178 - Non-alphabetical installation of requirements. Thanks Igor Sobreira * Fixed issue #199 - Documentation mentions --index instead of --index-url. Thanks Kelsey Hightower * Fixed issue #204 - rmtree undefined in mercurial.py. Thanks Kelsey Hightower * Fixed bug in Git vcs backend that would break during reinstallation. * Fixed bug in Mercurial vcs backend related to pip freeze and branch/tag resolution. * Fixed bug in version string parsing related to the suffix "-dev". 0.8.2 ----- * Avoid redundant unpacking of bundles (from pwaller) * Fixed issue #32, #150, #161 - Fixed checking out the correct tag/branch/commit when updating an editable Git requirement. * Fixed issue #49 - Added ability to install version control requirements without making them editable, e.g.:: pip install git+https://github.com/pypa/pip/ * Fixed issue #175 - Correctly locate build and source directory on Mac OS X. * Added ``git+https://`` scheme to Git VCS backend. 0.8.1 ----- * Added global --user flag as shortcut for --install-option="--user". From Ronny Pfannschmidt. * Added support for `PyPI mirrors `_ as defined in `PEP 381 `_, from Jannis Leidel. * Fixed issue #138 - Git revisions ignored. Thanks John-Scott Atlakson. * Fixed issue #95 - Initial editable install of github package from a tag fails. Thanks John-Scott Atlakson. * Fixed issue #107 - Can't install if a directory in cwd has the same name as the package you're installing. * Fixed issue #39 - --install-option="--prefix=~/.local" ignored with -e. Thanks Ronny Pfannschmidt and Wil Tan. 0.8 --- * Track which ``build/`` directories pip creates, never remove directories it doesn't create. From Hugo Lopes Tavares. * Pip now accepts file:// index URLs. Thanks Dave Abrahams. * Various cleanup to make test-running more consistent and less fragile. Thanks Dave Abrahams. * Real Windows support (with passing tests). Thanks Dave Abrahams. * ``pip-2.7`` etc. scripts are created (Python-version specific scripts) * ``contrib/build-standalone`` script creates a runnable ``.zip`` form of pip, from Jannis Leidel * Editable git repos are updated when reinstalled * Fix problem with ``--editable`` when multiple ``.egg-info/`` directories are found. * A number of VCS-related fixes for ``pip freeze``, from Hugo Lopes Tavares. * Significant test framework changes, from Hugo Lopes Tavares. 0.7.2 ----- * Set zip_safe=False to avoid problems some people are encountering where pip is installed as a zip file. 0.7.1 ----- * Fixed opening of logfile with no directory name. Thanks Alexandre Conrad. * Temporary files are consistently cleaned up, especially after installing bundles, also from Alex Conrad. * Tests now require at least ScriptTest 1.0.3. 0.7 --- * Fixed uninstallation on Windows * Added ``pip search`` command. * Tab-complete names of installed distributions for ``pip uninstall``. * Support tab-completion when there is a global-option before the subcommand. * Install header files in standard (scheme-default) location when installing outside a virtualenv. Install them to a slightly more consistent non-standard location inside a virtualenv (since the standard location is a non-writable symlink to the global location). * pip now logs to a central location by default (instead of creating ``pip-log.txt`` all over the place) and constantly overwrites the file in question. On Unix and Mac OS X this is ``'$HOME/.pip/pip.log'`` and on Windows it's ``'%HOME%\\pip\\pip.log'``. You are still able to override this location with the ``$PIP_LOG_FILE`` environment variable. For a complete (appended) logfile use the separate ``'--log'`` command line option. * Fixed an issue with Git that left an editable packge as a checkout of a remote branch, even if the default behaviour would have been fine, too. * Fixed installing from a Git tag with older versions of Git. * Expand "~" in logfile and download cache paths. * Speed up installing from Mercurial repositories by cloning without updating the working copy multiple times. * Fixed installing directly from directories (e.g. ``pip install path/to/dir/``). * Fixed installing editable packages with ``svn+ssh`` URLs. * Don't print unwanted debug information when running the freeze command. * Create log file directory automatically. Thanks Alexandre Conrad. * Make test suite easier to run successfully. Thanks Dave Abrahams. * Fixed "pip install ." and "pip install .."; better error for directory without setup.py. Thanks Alexandre Conrad. * Support Debian/Ubuntu "dist-packages" in zip command. Thanks duckx. * Fix relative --src folder. Thanks Simon Cross. * Handle missing VCS with an error message. Thanks Alexandre Conrad. * Added --no-download option to install; pairs with --no-install to separate download and installation into two steps. Thanks Simon Cross. * Fix uninstalling from requirements file containing -f, -i, or --extra-index-url. * Leftover build directories are now removed. Thanks Alexandre Conrad. 0.6.3 ----- * Fixed import error on Windows with regard to the backwards compatibility package 0.6.2 ----- * Fixed uninstall when /tmp is on a different filesystem. * Fixed uninstallation of distributions with namespace packages. 0.6.1 ----- * Added support for the ``https`` and ``http-static`` schemes to the Mercurial and ``ftp`` scheme to the Bazaar backend. * Fixed uninstallation of scripts installed with easy_install. * Fixed an issue in the package finder that could result in an infinite loop while looking for links. * Fixed issue with ``pip bundle`` and local files (which weren't being copied into the bundle), from Whit Morriss. 0.6 --- * Add ``pip uninstall`` and uninstall-before upgrade (from Carl Meyer). * Extended configurability with config files and environment variables. * Allow packages to be upgraded, e.g., ``pip install Package==0.1`` then ``pip install Package==0.2``. * Allow installing/upgrading to Package==dev (fix "Source version does not match target version" errors). * Added command and option completion for bash and zsh. * Extended integration with virtualenv by providing an option to automatically use an active virtualenv and an option to warn if no active virtualenv is found. * Fixed a bug with pip install --download and editable packages, where directories were being set with 0000 permissions, now defaults to 755. * Fixed uninstallation of easy_installed console_scripts. * Fixed uninstallation on Mac OS X Framework layout installs * Fixed bug preventing uninstall of editables with source outside venv. * Creates download cache directory if not existing. 0.5.1 ----- * Fixed a couple little bugs, with git and with extensions. 0.5 --- * Added ability to override the default log file name (``pip-log.txt``) with the environmental variable ``$PIP_LOG_FILE``. * Made the freeze command print installed packages to stdout instead of writing them to a file. Use simple redirection (e.g. ``pip freeze > stable-req.txt``) to get a file with requirements. * Fixed problem with freezing editable packages from a Git repository. * Added support for base URLs using ```` when parsing HTML pages. * Fixed installing of non-editable packages from version control systems. * Fixed issue with Bazaar's bzr+ssh scheme. * Added --download-dir option to the install command to retrieve package archives. If given an editable package it will create an archive of it. * Added ability to pass local file and directory paths to ``--find-links``, e.g. ``--find-links=file:///path/to/my/private/archive`` * Reduced the amount of console log messages when fetching a page to find a distribution was problematic. The full messages can be found in pip-log.txt. * Added ``--no-deps`` option to install ignore package dependencies * Added ``--no-index`` option to ignore the package index (PyPI) temporarily * Fixed installing editable packages from Git branches. * Fixes freezing of editable packages from Mercurial repositories. * Fixed handling read-only attributes of build files, e.g. of Subversion and Bazaar on Windows. * When downloading a file from a redirect, use the redirected location's extension to guess the compression (happens specifically when redirecting to a bitbucket.org tip.gz file). * Editable freeze URLs now always use revision hash/id rather than tip or branch names which could move. * Fixed comparison of repo URLs so incidental differences such as presence/absence of final slashes or quoted/unquoted special characters don't trigger "ignore/switch/wipe/backup" choice. * Fixed handling of attempt to checkout editable install to a non-empty, non-repo directory. 0.4 --- * Make ``-e`` work better with local hg repositories * Construct PyPI URLs the exact way easy_install constructs URLs (you might notice this if you use a custom index that is slash-sensitive). * Improvements on Windows (from `Ionel Maries Cristian `_). * Fixed problem with not being able to install private git repositories. * Make ``pip zip`` zip all its arguments, not just the first. * Fix some filename issues on Windows. * Allow the ``-i`` and ``--extra-index-url`` options in requirements files. * Fix the way bundle components are unpacked and moved around, to make bundles work. * Adds ``-s`` option to allow the access to the global site-packages if a virtualenv is to be created. * Fixed support for Subversion 1.6. 0.3.1 ----- * Improved virtualenv restart and various path/cleanup problems on win32. * Fixed a regression with installing from svn repositories (when not using ``-e``). * Fixes when installing editable packages that put their source in a subdirectory (like ``src/``). * Improve ``pip -h`` 0.3 --- * Added support for editable packages created from Git, Mercurial and Bazaar repositories and ability to freeze them. Refactored support for version control systems. * Do not use ``sys.exit()`` from inside the code, instead use a return. This will make it easier to invoke programmatically. * Put the install record in ``Package.egg-info/installed-files.txt`` (previously they went in ``site-packages/install-record-Package.txt``). * Fix a problem with ``pip freeze`` not including ``-e svn+`` when an svn structure is peculiar. * Allow ``pip -E`` to work with a virtualenv that uses a different version of Python than the parent environment. * Fixed Win32 virtualenv (``-E``) option. * Search the links passed in with ``-f`` for packages. * Detect zip files, even when the file doesn't have a ``.zip`` extension and it is served with the wrong Content-Type. * Installing editable from existing source now works, like ``pip install -e some/path/`` will install the package in ``some/path/``. Most importantly, anything that package requires will also be installed by pip. * Add a ``--path`` option to ``pip un/zip``, so you can avoid zipping files that are outside of where you expect. * Add ``--simulate`` option to ``pip zip``. 0.2.1 ----- * Fixed small problem that prevented using ``pip.py`` without actually installing pip. * Fixed ``--upgrade``, which would download and appear to install upgraded packages, but actually just reinstall the existing package. * Fixed Windows problem with putting the install record in the right place, and generating the ``pip`` script with Setuptools. * Download links that include embedded spaces or other unsafe characters (those characters get %-encoded). * Fixed use of URLs in requirement files, and problems with some blank lines. * Turn some tar file errors into warnings. 0.2 --- * Renamed to ``pip``, and to install you now do ``pip install PACKAGE`` * Added command ``pip zip PACKAGE`` and ``pip unzip PACKAGE``. This is particularly intended for Google App Engine to manage libraries to stay under the 1000-file limit. * Some fixes to bundles, especially editable packages and when creating a bundle using unnamed packages (like just an svn repository without ``#egg=Package``). 0.1.4 ----- * Added an option ``--install-option`` to pass options to pass arguments to ``setup.py install`` * ``.svn/`` directories are no longer included in bundles, as these directories are specific to a version of svn -- if you build a bundle on a system with svn 1.5, you can't use the checkout on a system with svn 1.4. Instead a file ``svn-checkout.txt`` is included that notes the original location and revision, and the command you can use to turn it back into an svn checkout. (Probably unpacking the bundle should, maybe optionally, recreate this information -- but that is not currently implemented, and it would require network access.) * Avoid ambiguities over project name case, where for instance MyPackage and mypackage would be considered different packages. This in particular caused problems on Macs, where ``MyPackage/`` and ``mypackage/`` are the same directory. * Added support for an environmental variable ``$PIP_DOWNLOAD_CACHE`` which will cache package downloads, so future installations won't require large downloads. Network access is still required, but just some downloads will be avoided when using this. 0.1.3 ----- * Always use ``svn checkout`` (not ``export``) so that ``tag_svn_revision`` settings give the revision of the package. * Don't update checkouts that came from ``.pybundle`` files. 0.1.2 ----- * Improve error text when there are errors fetching HTML pages when seeking packages. * Improve bundles: include empty directories, make them work with editable packages. * If you use ``-E env`` and the environment ``env/`` doesn't exist, a new virtual environment will be created. * Fix ``dependency_links`` for finding packages. 0.1.1 ----- * Fixed a NameError exception when running pip outside of a virtualenv environment. * Added HTTP proxy support (from Prabhu Ramachandran) * Fixed use of ``hashlib.md5`` on python2.5+ (also from Prabhu Ramachandran) 0.1 --- * Initial release Keywords: easy_install distutils setuptools egg virtualenv Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Build Tools Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO/SOURCES.txt000066400000000000000000000017151256046442300313010ustar00rootroot00000000000000AUTHORS.txt LICENSE.txt MANIFEST.in setup.cfg setup.py docs/ci-server-step-by-step.txt docs/configuration.txt docs/contributing.txt docs/glossary.txt docs/index.txt docs/installing.txt docs/news.txt docs/other-tools.txt docs/requirements.txt docs/usage.txt pip/__init__.py pip/_pkgutil.py pip/backwardcompat.py pip/basecommand.py pip/baseparser.py pip/download.py pip/exceptions.py pip/index.py pip/locations.py pip/log.py pip/req.py pip/runner.py pip/status_codes.py pip/util.py pip.egg-info/PKG-INFO pip.egg-info/SOURCES.txt pip.egg-info/dependency_links.txt pip.egg-info/entry_points.txt pip.egg-info/not-zip-safe pip.egg-info/top_level.txt pip/commands/__init__.py pip/commands/bundle.py pip/commands/completion.py pip/commands/freeze.py pip/commands/help.py pip/commands/install.py pip/commands/search.py pip/commands/uninstall.py pip/commands/unzip.py pip/commands/zip.py pip/vcs/__init__.py pip/vcs/bazaar.py pip/vcs/git.py pip/vcs/mercurial.py pip/vcs/subversion.pydependency_links.txt000066400000000000000000000000011256046442300334000ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO entry_points.txt000066400000000000000000000000651256046442300326310ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO[console_scripts] pip = pip:main pip-2.7 = pip:main circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO/not-zip-safe000066400000000000000000000000011256046442300316370ustar00rootroot00000000000000 circus-0.12.1/circus/tests/venv/lib/python2.7/site-packages/pip-7.7-py2.7.egg/EGG-INFO/top_level.txt000066400000000000000000000000041256046442300321350ustar00rootroot00000000000000pip circus-0.12.1/circus/tests/venv/lib/python3.2/000077500000000000000000000000001256046442300210135ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/no-global-site-packages.txt000066400000000000000000000000001256046442300261320ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/orig-prefix.txt000066400000000000000000000000611256046442300240040ustar00rootroot00000000000000/Library/Frameworks/Python.framework/Versions/3.2circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/000077500000000000000000000000001256046442300235335ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/easy-install.pth000066400000000000000000000003151256046442300266540ustar00rootroot00000000000000import sys; sys.__plen = len(sys.path) ./pip-7.7-py3.2.egg import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/000077500000000000000000000000001256046442300262465ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO/000077500000000000000000000000001256046442300274015ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO/PKG-INFO000066400000000000000000000624131256046442300305040ustar00rootroot00000000000000Metadata-Version: 1.0 Name: pip Version: 1.1 Summary: pip installs packages. Python packages. An easy_install replacement Home-page: http://www.pip-installer.org Author: The pip developers Author-email: python-virtualenv@groups.google.com License: MIT Description: pip === `pip` is a tool for installing and managing Python packages, such as those found in the `Python Package Index`_. It's a replacement for easy_install_. :: $ pip install simplejson [... progress report ...] Successfully installed simplejson .. _`Python Package Index`: http://pypi.python.org/pypi .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall Upgrading a package:: $ pip install --upgrade simplejson [... progress report ...] Successfully installed simplejson Removing a package:: $ pip uninstall simplejson Uninstalling simplejson: /home/me/env/lib/python2.7/site-packages/simplejson /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info Proceed (y/n)? y Successfully uninstalled simplejson .. comment: The main website for pip is `www.pip-installer.org `_. You can also install the `in-development version `_ of pip with ``easy_install pip==dev``. Community --------- The homepage for pip is at `pip-installer.org `_. Bugs can be filed in the `pip issue tracker `_. Discussion happens on the `virtualenv email group `_. ==== News ==== Changelog ========= Next release (1.2) schedule --------------------------- Beta and final releases planned for the second half of 2012. 1.1 (2012-02-16) ---------------- * Fixed issue #326 - don't crash when a package's setup.py emits UTF-8 and then fails. Thanks Marc Abramowitz. * Added ``--target`` option for installing directly to arbitrary directory. Thanks Stavros Korokithakis. * Added support for authentication with Subversion repositories. Thanks Qiangning Hong. * Fixed issue #315 - ``--download`` now downloads dependencies as well. Thanks Qiangning Hong. * Errors from subprocesses will display the current working directory. Thanks Antti Kaihola. * Fixed issue #369 - compatibility with Subversion 1.7. Thanks Qiangning Hong. Note that setuptools remains incompatible with Subversion 1.7; to get the benefits of pip's support you must use Distribute rather than setuptools. * Fixed issue #57 - ignore py2app-generated OS X mpkg zip files in finder. Thanks Rene Dudfield. * Fixed issue #182 - log to ~/Library/Logs/ by default on OS X framework installs. Thanks Dan Callahan for report and patch. * Fixed issue #310 - understand version tags without minor version ("py3") in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for patch. * Fixed issue #7 - Pip now supports optionally installing setuptools "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker and Olivier Girardot. * Fixed issue #391 - freeze no longer borks on requirements files with --index-url or --find-links. Thanks Herbert Pfennig. * Fixed issue #288 - handle symlinks properly. Thanks lebedov for the patch. * Fixed issue #49 - pip install -U no longer reinstalls the same versions of packages. Thanks iguananaut for the pull request. * Removed ``-E`` option and ``PIP_RESPECT_VIRTUALENV``; both use a restart-in-venv mechanism that's broken, and neither one is useful since every virtualenv now has pip inside it. * Fixed issue #366 - pip throws IndexError when it calls `scraped_rel_links` * Fixed issue #22 - pip search should set and return a userful shell status code * Fixed issue #351 and #365 - added global ``--exists-action`` command line option to easier script file exists conflicts, e.g. from editable requirements from VCS that have a changed repo URL. 1.0.2 (2011-07-16) ------------------ * Fixed docs issues. * Fixed issue #295 - Reinstall a package when using the ``install -I`` option * Fixed issue #283 - Finds a Git tag pointing to same commit as origin/master * Fixed issue #279 - Use absolute path for path to docs in setup.py * Fixed issue #314 - Correctly handle exceptions on Python3. * Fixed issue #320 - Correctly parse ``--editable`` lines in requirements files 1.0.1 (2011-04-30) ------------------ * Start to use git-flow. * Fixed issue #274 - `find_command` should not raise AttributeError * Fixed issue #273 - respect Content-Disposition header. Thanks Bradley Ayers. * Fixed issue #233 - pathext handling on Windows. * Fixed issue #252 - svn+svn protocol. * Fixed issue #44 - multiple CLI searches. * Fixed issue #266 - current working directory when running setup.py clean. 1.0 (2011-04-04) ---------------- * Added Python 3 support! Huge thanks to Vinay Sajip, Vitaly Babiy, Kelsey Hightower, and Alex Gronholm, among others. * Download progress only shown on a real TTY. Thanks Alex Morega. * Fixed finding of VCS binaries to not be fooled by same-named directories. Thanks Alex Morega. * Fixed uninstall of packages from system Python for users of Debian/Ubuntu python-setuptools package (workaround until fixed in Debian and Ubuntu). * Added `get-pip.py `_ installer. Simply download and execute it, using the Python interpreter of your choice:: $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py $ python get-pip.py This may have to be run as root. .. note:: Make sure you have `distribute `_ installed before using the installer! 0.8.3 ----- * Moved main repository to Github: https://github.com/pypa/pip * Transferred primary maintenance from Ian to Jannis Leidel, Carl Meyer, Brian Rosner * Fixed issue #14 - No uninstall-on-upgrade with URL package. Thanks Oliver Tonnhofer * Fixed issue #163 - Egg name not properly resolved. Thanks Igor Sobreira * Fixed issue #178 - Non-alphabetical installation of requirements. Thanks Igor Sobreira * Fixed issue #199 - Documentation mentions --index instead of --index-url. Thanks Kelsey Hightower * Fixed issue #204 - rmtree undefined in mercurial.py. Thanks Kelsey Hightower * Fixed bug in Git vcs backend that would break during reinstallation. * Fixed bug in Mercurial vcs backend related to pip freeze and branch/tag resolution. * Fixed bug in version string parsing related to the suffix "-dev". 0.8.2 ----- * Avoid redundant unpacking of bundles (from pwaller) * Fixed issue #32, #150, #161 - Fixed checking out the correct tag/branch/commit when updating an editable Git requirement. * Fixed issue #49 - Added ability to install version control requirements without making them editable, e.g.:: pip install git+https://github.com/pypa/pip/ * Fixed issue #175 - Correctly locate build and source directory on Mac OS X. * Added ``git+https://`` scheme to Git VCS backend. 0.8.1 ----- * Added global --user flag as shortcut for --install-option="--user". From Ronny Pfannschmidt. * Added support for `PyPI mirrors `_ as defined in `PEP 381 `_, from Jannis Leidel. * Fixed issue #138 - Git revisions ignored. Thanks John-Scott Atlakson. * Fixed issue #95 - Initial editable install of github package from a tag fails. Thanks John-Scott Atlakson. * Fixed issue #107 - Can't install if a directory in cwd has the same name as the package you're installing. * Fixed issue #39 - --install-option="--prefix=~/.local" ignored with -e. Thanks Ronny Pfannschmidt and Wil Tan. 0.8 --- * Track which ``build/`` directories pip creates, never remove directories it doesn't create. From Hugo Lopes Tavares. * Pip now accepts file:// index URLs. Thanks Dave Abrahams. * Various cleanup to make test-running more consistent and less fragile. Thanks Dave Abrahams. * Real Windows support (with passing tests). Thanks Dave Abrahams. * ``pip-2.7`` etc. scripts are created (Python-version specific scripts) * ``contrib/build-standalone`` script creates a runnable ``.zip`` form of pip, from Jannis Leidel * Editable git repos are updated when reinstalled * Fix problem with ``--editable`` when multiple ``.egg-info/`` directories are found. * A number of VCS-related fixes for ``pip freeze``, from Hugo Lopes Tavares. * Significant test framework changes, from Hugo Lopes Tavares. 0.7.2 ----- * Set zip_safe=False to avoid problems some people are encountering where pip is installed as a zip file. 0.7.1 ----- * Fixed opening of logfile with no directory name. Thanks Alexandre Conrad. * Temporary files are consistently cleaned up, especially after installing bundles, also from Alex Conrad. * Tests now require at least ScriptTest 1.0.3. 0.7 --- * Fixed uninstallation on Windows * Added ``pip search`` command. * Tab-complete names of installed distributions for ``pip uninstall``. * Support tab-completion when there is a global-option before the subcommand. * Install header files in standard (scheme-default) location when installing outside a virtualenv. Install them to a slightly more consistent non-standard location inside a virtualenv (since the standard location is a non-writable symlink to the global location). * pip now logs to a central location by default (instead of creating ``pip-log.txt`` all over the place) and constantly overwrites the file in question. On Unix and Mac OS X this is ``'$HOME/.pip/pip.log'`` and on Windows it's ``'%HOME%\\pip\\pip.log'``. You are still able to override this location with the ``$PIP_LOG_FILE`` environment variable. For a complete (appended) logfile use the separate ``'--log'`` command line option. * Fixed an issue with Git that left an editable packge as a checkout of a remote branch, even if the default behaviour would have been fine, too. * Fixed installing from a Git tag with older versions of Git. * Expand "~" in logfile and download cache paths. * Speed up installing from Mercurial repositories by cloning without updating the working copy multiple times. * Fixed installing directly from directories (e.g. ``pip install path/to/dir/``). * Fixed installing editable packages with ``svn+ssh`` URLs. * Don't print unwanted debug information when running the freeze command. * Create log file directory automatically. Thanks Alexandre Conrad. * Make test suite easier to run successfully. Thanks Dave Abrahams. * Fixed "pip install ." and "pip install .."; better error for directory without setup.py. Thanks Alexandre Conrad. * Support Debian/Ubuntu "dist-packages" in zip command. Thanks duckx. * Fix relative --src folder. Thanks Simon Cross. * Handle missing VCS with an error message. Thanks Alexandre Conrad. * Added --no-download option to install; pairs with --no-install to separate download and installation into two steps. Thanks Simon Cross. * Fix uninstalling from requirements file containing -f, -i, or --extra-index-url. * Leftover build directories are now removed. Thanks Alexandre Conrad. 0.6.3 ----- * Fixed import error on Windows with regard to the backwards compatibility package 0.6.2 ----- * Fixed uninstall when /tmp is on a different filesystem. * Fixed uninstallation of distributions with namespace packages. 0.6.1 ----- * Added support for the ``https`` and ``http-static`` schemes to the Mercurial and ``ftp`` scheme to the Bazaar backend. * Fixed uninstallation of scripts installed with easy_install. * Fixed an issue in the package finder that could result in an infinite loop while looking for links. * Fixed issue with ``pip bundle`` and local files (which weren't being copied into the bundle), from Whit Morriss. 0.6 --- * Add ``pip uninstall`` and uninstall-before upgrade (from Carl Meyer). * Extended configurability with config files and environment variables. * Allow packages to be upgraded, e.g., ``pip install Package==0.1`` then ``pip install Package==0.2``. * Allow installing/upgrading to Package==dev (fix "Source version does not match target version" errors). * Added command and option completion for bash and zsh. * Extended integration with virtualenv by providing an option to automatically use an active virtualenv and an option to warn if no active virtualenv is found. * Fixed a bug with pip install --download and editable packages, where directories were being set with 0000 permissions, now defaults to 755. * Fixed uninstallation of easy_installed console_scripts. * Fixed uninstallation on Mac OS X Framework layout installs * Fixed bug preventing uninstall of editables with source outside venv. * Creates download cache directory if not existing. 0.5.1 ----- * Fixed a couple little bugs, with git and with extensions. 0.5 --- * Added ability to override the default log file name (``pip-log.txt``) with the environmental variable ``$PIP_LOG_FILE``. * Made the freeze command print installed packages to stdout instead of writing them to a file. Use simple redirection (e.g. ``pip freeze > stable-req.txt``) to get a file with requirements. * Fixed problem with freezing editable packages from a Git repository. * Added support for base URLs using ```` when parsing HTML pages. * Fixed installing of non-editable packages from version control systems. * Fixed issue with Bazaar's bzr+ssh scheme. * Added --download-dir option to the install command to retrieve package archives. If given an editable package it will create an archive of it. * Added ability to pass local file and directory paths to ``--find-links``, e.g. ``--find-links=file:///path/to/my/private/archive`` * Reduced the amount of console log messages when fetching a page to find a distribution was problematic. The full messages can be found in pip-log.txt. * Added ``--no-deps`` option to install ignore package dependencies * Added ``--no-index`` option to ignore the package index (PyPI) temporarily * Fixed installing editable packages from Git branches. * Fixes freezing of editable packages from Mercurial repositories. * Fixed handling read-only attributes of build files, e.g. of Subversion and Bazaar on Windows. * When downloading a file from a redirect, use the redirected location's extension to guess the compression (happens specifically when redirecting to a bitbucket.org tip.gz file). * Editable freeze URLs now always use revision hash/id rather than tip or branch names which could move. * Fixed comparison of repo URLs so incidental differences such as presence/absence of final slashes or quoted/unquoted special characters don't trigger "ignore/switch/wipe/backup" choice. * Fixed handling of attempt to checkout editable install to a non-empty, non-repo directory. 0.4 --- * Make ``-e`` work better with local hg repositories * Construct PyPI URLs the exact way easy_install constructs URLs (you might notice this if you use a custom index that is slash-sensitive). * Improvements on Windows (from `Ionel Maries Cristian `_). * Fixed problem with not being able to install private git repositories. * Make ``pip zip`` zip all its arguments, not just the first. * Fix some filename issues on Windows. * Allow the ``-i`` and ``--extra-index-url`` options in requirements files. * Fix the way bundle components are unpacked and moved around, to make bundles work. * Adds ``-s`` option to allow the access to the global site-packages if a virtualenv is to be created. * Fixed support for Subversion 1.6. 0.3.1 ----- * Improved virtualenv restart and various path/cleanup problems on win32. * Fixed a regression with installing from svn repositories (when not using ``-e``). * Fixes when installing editable packages that put their source in a subdirectory (like ``src/``). * Improve ``pip -h`` 0.3 --- * Added support for editable packages created from Git, Mercurial and Bazaar repositories and ability to freeze them. Refactored support for version control systems. * Do not use ``sys.exit()`` from inside the code, instead use a return. This will make it easier to invoke programmatically. * Put the install record in ``Package.egg-info/installed-files.txt`` (previously they went in ``site-packages/install-record-Package.txt``). * Fix a problem with ``pip freeze`` not including ``-e svn+`` when an svn structure is peculiar. * Allow ``pip -E`` to work with a virtualenv that uses a different version of Python than the parent environment. * Fixed Win32 virtualenv (``-E``) option. * Search the links passed in with ``-f`` for packages. * Detect zip files, even when the file doesn't have a ``.zip`` extension and it is served with the wrong Content-Type. * Installing editable from existing source now works, like ``pip install -e some/path/`` will install the package in ``some/path/``. Most importantly, anything that package requires will also be installed by pip. * Add a ``--path`` option to ``pip un/zip``, so you can avoid zipping files that are outside of where you expect. * Add ``--simulate`` option to ``pip zip``. 0.2.1 ----- * Fixed small problem that prevented using ``pip.py`` without actually installing pip. * Fixed ``--upgrade``, which would download and appear to install upgraded packages, but actually just reinstall the existing package. * Fixed Windows problem with putting the install record in the right place, and generating the ``pip`` script with Setuptools. * Download links that include embedded spaces or other unsafe characters (those characters get %-encoded). * Fixed use of URLs in requirement files, and problems with some blank lines. * Turn some tar file errors into warnings. 0.2 --- * Renamed to ``pip``, and to install you now do ``pip install PACKAGE`` * Added command ``pip zip PACKAGE`` and ``pip unzip PACKAGE``. This is particularly intended for Google App Engine to manage libraries to stay under the 1000-file limit. * Some fixes to bundles, especially editable packages and when creating a bundle using unnamed packages (like just an svn repository without ``#egg=Package``). 0.1.4 ----- * Added an option ``--install-option`` to pass options to pass arguments to ``setup.py install`` * ``.svn/`` directories are no longer included in bundles, as these directories are specific to a version of svn -- if you build a bundle on a system with svn 1.5, you can't use the checkout on a system with svn 1.4. Instead a file ``svn-checkout.txt`` is included that notes the original location and revision, and the command you can use to turn it back into an svn checkout. (Probably unpacking the bundle should, maybe optionally, recreate this information -- but that is not currently implemented, and it would require network access.) * Avoid ambiguities over project name case, where for instance MyPackage and mypackage would be considered different packages. This in particular caused problems on Macs, where ``MyPackage/`` and ``mypackage/`` are the same directory. * Added support for an environmental variable ``$PIP_DOWNLOAD_CACHE`` which will cache package downloads, so future installations won't require large downloads. Network access is still required, but just some downloads will be avoided when using this. 0.1.3 ----- * Always use ``svn checkout`` (not ``export``) so that ``tag_svn_revision`` settings give the revision of the package. * Don't update checkouts that came from ``.pybundle`` files. 0.1.2 ----- * Improve error text when there are errors fetching HTML pages when seeking packages. * Improve bundles: include empty directories, make them work with editable packages. * If you use ``-E env`` and the environment ``env/`` doesn't exist, a new virtual environment will be created. * Fix ``dependency_links`` for finding packages. 0.1.1 ----- * Fixed a NameError exception when running pip outside of a virtualenv environment. * Added HTTP proxy support (from Prabhu Ramachandran) * Fixed use of ``hashlib.md5`` on python2.5+ (also from Prabhu Ramachandran) 0.1 --- * Initial release Keywords: easy_install distutils setuptools egg virtualenv Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Build Tools Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO/SOURCES.txt000066400000000000000000000017151256046442300312710ustar00rootroot00000000000000AUTHORS.txt LICENSE.txt MANIFEST.in setup.cfg setup.py docs/ci-server-step-by-step.txt docs/configuration.txt docs/contributing.txt docs/glossary.txt docs/index.txt docs/installing.txt docs/news.txt docs/other-tools.txt docs/requirements.txt docs/usage.txt pip/__init__.py pip/_pkgutil.py pip/backwardcompat.py pip/basecommand.py pip/baseparser.py pip/download.py pip/exceptions.py pip/index.py pip/locations.py pip/log.py pip/req.py pip/runner.py pip/status_codes.py pip/util.py pip.egg-info/PKG-INFO pip.egg-info/SOURCES.txt pip.egg-info/dependency_links.txt pip.egg-info/entry_points.txt pip.egg-info/not-zip-safe pip.egg-info/top_level.txt pip/commands/__init__.py pip/commands/bundle.py pip/commands/completion.py pip/commands/freeze.py pip/commands/help.py pip/commands/install.py pip/commands/search.py pip/commands/uninstall.py pip/commands/unzip.py pip/commands/zip.py pip/vcs/__init__.py pip/vcs/bazaar.py pip/vcs/git.py pip/vcs/mercurial.py pip/vcs/subversion.pydependency_links.txt000066400000000000000000000000011256046442300333700ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO entry_points.txt000066400000000000000000000000651256046442300326210ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO[console_scripts] pip = pip:main pip-2.7 = pip:main circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO/not-zip-safe000066400000000000000000000000011256046442300316270ustar00rootroot00000000000000 circus-0.12.1/circus/tests/venv/lib/python3.2/site-packages/pip-7.7-py3.2.egg/EGG-INFO/top_level.txt000066400000000000000000000000041256046442300321250ustar00rootroot00000000000000pip circus-0.12.1/circus/tests/venv/lib/python3.3/000077500000000000000000000000001256046442300210145ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/no-global-site-packages.txt000066400000000000000000000000001256046442300261330ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/orig-prefix.txt000066400000000000000000000000611256046442300240050ustar00rootroot00000000000000/Library/Frameworks/Python.framework/Versions/3.3circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/000077500000000000000000000000001256046442300235345ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/easy-install.pth000066400000000000000000000003151256046442300266550ustar00rootroot00000000000000import sys; sys.__plen = len(sys.path) ./pip-7.7-py3.3.egg import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/000077500000000000000000000000001256046442300262505ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO/000077500000000000000000000000001256046442300274035ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO/PKG-INFO000066400000000000000000000624131256046442300305060ustar00rootroot00000000000000Metadata-Version: 1.0 Name: pip Version: 1.1 Summary: pip installs packages. Python packages. An easy_install replacement Home-page: http://www.pip-installer.org Author: The pip developers Author-email: python-virtualenv@groups.google.com License: MIT Description: pip === `pip` is a tool for installing and managing Python packages, such as those found in the `Python Package Index`_. It's a replacement for easy_install_. :: $ pip install simplejson [... progress report ...] Successfully installed simplejson .. _`Python Package Index`: http://pypi.python.org/pypi .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall Upgrading a package:: $ pip install --upgrade simplejson [... progress report ...] Successfully installed simplejson Removing a package:: $ pip uninstall simplejson Uninstalling simplejson: /home/me/env/lib/python2.7/site-packages/simplejson /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info Proceed (y/n)? y Successfully uninstalled simplejson .. comment: The main website for pip is `www.pip-installer.org `_. You can also install the `in-development version `_ of pip with ``easy_install pip==dev``. Community --------- The homepage for pip is at `pip-installer.org `_. Bugs can be filed in the `pip issue tracker `_. Discussion happens on the `virtualenv email group `_. ==== News ==== Changelog ========= Next release (1.2) schedule --------------------------- Beta and final releases planned for the second half of 2012. 1.1 (2012-02-16) ---------------- * Fixed issue #326 - don't crash when a package's setup.py emits UTF-8 and then fails. Thanks Marc Abramowitz. * Added ``--target`` option for installing directly to arbitrary directory. Thanks Stavros Korokithakis. * Added support for authentication with Subversion repositories. Thanks Qiangning Hong. * Fixed issue #315 - ``--download`` now downloads dependencies as well. Thanks Qiangning Hong. * Errors from subprocesses will display the current working directory. Thanks Antti Kaihola. * Fixed issue #369 - compatibility with Subversion 1.7. Thanks Qiangning Hong. Note that setuptools remains incompatible with Subversion 1.7; to get the benefits of pip's support you must use Distribute rather than setuptools. * Fixed issue #57 - ignore py2app-generated OS X mpkg zip files in finder. Thanks Rene Dudfield. * Fixed issue #182 - log to ~/Library/Logs/ by default on OS X framework installs. Thanks Dan Callahan for report and patch. * Fixed issue #310 - understand version tags without minor version ("py3") in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for patch. * Fixed issue #7 - Pip now supports optionally installing setuptools "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker and Olivier Girardot. * Fixed issue #391 - freeze no longer borks on requirements files with --index-url or --find-links. Thanks Herbert Pfennig. * Fixed issue #288 - handle symlinks properly. Thanks lebedov for the patch. * Fixed issue #49 - pip install -U no longer reinstalls the same versions of packages. Thanks iguananaut for the pull request. * Removed ``-E`` option and ``PIP_RESPECT_VIRTUALENV``; both use a restart-in-venv mechanism that's broken, and neither one is useful since every virtualenv now has pip inside it. * Fixed issue #366 - pip throws IndexError when it calls `scraped_rel_links` * Fixed issue #22 - pip search should set and return a userful shell status code * Fixed issue #351 and #365 - added global ``--exists-action`` command line option to easier script file exists conflicts, e.g. from editable requirements from VCS that have a changed repo URL. 1.0.2 (2011-07-16) ------------------ * Fixed docs issues. * Fixed issue #295 - Reinstall a package when using the ``install -I`` option * Fixed issue #283 - Finds a Git tag pointing to same commit as origin/master * Fixed issue #279 - Use absolute path for path to docs in setup.py * Fixed issue #314 - Correctly handle exceptions on Python3. * Fixed issue #320 - Correctly parse ``--editable`` lines in requirements files 1.0.1 (2011-04-30) ------------------ * Start to use git-flow. * Fixed issue #274 - `find_command` should not raise AttributeError * Fixed issue #273 - respect Content-Disposition header. Thanks Bradley Ayers. * Fixed issue #233 - pathext handling on Windows. * Fixed issue #252 - svn+svn protocol. * Fixed issue #44 - multiple CLI searches. * Fixed issue #266 - current working directory when running setup.py clean. 1.0 (2011-04-04) ---------------- * Added Python 3 support! Huge thanks to Vinay Sajip, Vitaly Babiy, Kelsey Hightower, and Alex Gronholm, among others. * Download progress only shown on a real TTY. Thanks Alex Morega. * Fixed finding of VCS binaries to not be fooled by same-named directories. Thanks Alex Morega. * Fixed uninstall of packages from system Python for users of Debian/Ubuntu python-setuptools package (workaround until fixed in Debian and Ubuntu). * Added `get-pip.py `_ installer. Simply download and execute it, using the Python interpreter of your choice:: $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py $ python get-pip.py This may have to be run as root. .. note:: Make sure you have `distribute `_ installed before using the installer! 0.8.3 ----- * Moved main repository to Github: https://github.com/pypa/pip * Transferred primary maintenance from Ian to Jannis Leidel, Carl Meyer, Brian Rosner * Fixed issue #14 - No uninstall-on-upgrade with URL package. Thanks Oliver Tonnhofer * Fixed issue #163 - Egg name not properly resolved. Thanks Igor Sobreira * Fixed issue #178 - Non-alphabetical installation of requirements. Thanks Igor Sobreira * Fixed issue #199 - Documentation mentions --index instead of --index-url. Thanks Kelsey Hightower * Fixed issue #204 - rmtree undefined in mercurial.py. Thanks Kelsey Hightower * Fixed bug in Git vcs backend that would break during reinstallation. * Fixed bug in Mercurial vcs backend related to pip freeze and branch/tag resolution. * Fixed bug in version string parsing related to the suffix "-dev". 0.8.2 ----- * Avoid redundant unpacking of bundles (from pwaller) * Fixed issue #32, #150, #161 - Fixed checking out the correct tag/branch/commit when updating an editable Git requirement. * Fixed issue #49 - Added ability to install version control requirements without making them editable, e.g.:: pip install git+https://github.com/pypa/pip/ * Fixed issue #175 - Correctly locate build and source directory on Mac OS X. * Added ``git+https://`` scheme to Git VCS backend. 0.8.1 ----- * Added global --user flag as shortcut for --install-option="--user". From Ronny Pfannschmidt. * Added support for `PyPI mirrors `_ as defined in `PEP 381 `_, from Jannis Leidel. * Fixed issue #138 - Git revisions ignored. Thanks John-Scott Atlakson. * Fixed issue #95 - Initial editable install of github package from a tag fails. Thanks John-Scott Atlakson. * Fixed issue #107 - Can't install if a directory in cwd has the same name as the package you're installing. * Fixed issue #39 - --install-option="--prefix=~/.local" ignored with -e. Thanks Ronny Pfannschmidt and Wil Tan. 0.8 --- * Track which ``build/`` directories pip creates, never remove directories it doesn't create. From Hugo Lopes Tavares. * Pip now accepts file:// index URLs. Thanks Dave Abrahams. * Various cleanup to make test-running more consistent and less fragile. Thanks Dave Abrahams. * Real Windows support (with passing tests). Thanks Dave Abrahams. * ``pip-2.7`` etc. scripts are created (Python-version specific scripts) * ``contrib/build-standalone`` script creates a runnable ``.zip`` form of pip, from Jannis Leidel * Editable git repos are updated when reinstalled * Fix problem with ``--editable`` when multiple ``.egg-info/`` directories are found. * A number of VCS-related fixes for ``pip freeze``, from Hugo Lopes Tavares. * Significant test framework changes, from Hugo Lopes Tavares. 0.7.2 ----- * Set zip_safe=False to avoid problems some people are encountering where pip is installed as a zip file. 0.7.1 ----- * Fixed opening of logfile with no directory name. Thanks Alexandre Conrad. * Temporary files are consistently cleaned up, especially after installing bundles, also from Alex Conrad. * Tests now require at least ScriptTest 1.0.3. 0.7 --- * Fixed uninstallation on Windows * Added ``pip search`` command. * Tab-complete names of installed distributions for ``pip uninstall``. * Support tab-completion when there is a global-option before the subcommand. * Install header files in standard (scheme-default) location when installing outside a virtualenv. Install them to a slightly more consistent non-standard location inside a virtualenv (since the standard location is a non-writable symlink to the global location). * pip now logs to a central location by default (instead of creating ``pip-log.txt`` all over the place) and constantly overwrites the file in question. On Unix and Mac OS X this is ``'$HOME/.pip/pip.log'`` and on Windows it's ``'%HOME%\\pip\\pip.log'``. You are still able to override this location with the ``$PIP_LOG_FILE`` environment variable. For a complete (appended) logfile use the separate ``'--log'`` command line option. * Fixed an issue with Git that left an editable packge as a checkout of a remote branch, even if the default behaviour would have been fine, too. * Fixed installing from a Git tag with older versions of Git. * Expand "~" in logfile and download cache paths. * Speed up installing from Mercurial repositories by cloning without updating the working copy multiple times. * Fixed installing directly from directories (e.g. ``pip install path/to/dir/``). * Fixed installing editable packages with ``svn+ssh`` URLs. * Don't print unwanted debug information when running the freeze command. * Create log file directory automatically. Thanks Alexandre Conrad. * Make test suite easier to run successfully. Thanks Dave Abrahams. * Fixed "pip install ." and "pip install .."; better error for directory without setup.py. Thanks Alexandre Conrad. * Support Debian/Ubuntu "dist-packages" in zip command. Thanks duckx. * Fix relative --src folder. Thanks Simon Cross. * Handle missing VCS with an error message. Thanks Alexandre Conrad. * Added --no-download option to install; pairs with --no-install to separate download and installation into two steps. Thanks Simon Cross. * Fix uninstalling from requirements file containing -f, -i, or --extra-index-url. * Leftover build directories are now removed. Thanks Alexandre Conrad. 0.6.3 ----- * Fixed import error on Windows with regard to the backwards compatibility package 0.6.2 ----- * Fixed uninstall when /tmp is on a different filesystem. * Fixed uninstallation of distributions with namespace packages. 0.6.1 ----- * Added support for the ``https`` and ``http-static`` schemes to the Mercurial and ``ftp`` scheme to the Bazaar backend. * Fixed uninstallation of scripts installed with easy_install. * Fixed an issue in the package finder that could result in an infinite loop while looking for links. * Fixed issue with ``pip bundle`` and local files (which weren't being copied into the bundle), from Whit Morriss. 0.6 --- * Add ``pip uninstall`` and uninstall-before upgrade (from Carl Meyer). * Extended configurability with config files and environment variables. * Allow packages to be upgraded, e.g., ``pip install Package==0.1`` then ``pip install Package==0.2``. * Allow installing/upgrading to Package==dev (fix "Source version does not match target version" errors). * Added command and option completion for bash and zsh. * Extended integration with virtualenv by providing an option to automatically use an active virtualenv and an option to warn if no active virtualenv is found. * Fixed a bug with pip install --download and editable packages, where directories were being set with 0000 permissions, now defaults to 755. * Fixed uninstallation of easy_installed console_scripts. * Fixed uninstallation on Mac OS X Framework layout installs * Fixed bug preventing uninstall of editables with source outside venv. * Creates download cache directory if not existing. 0.5.1 ----- * Fixed a couple little bugs, with git and with extensions. 0.5 --- * Added ability to override the default log file name (``pip-log.txt``) with the environmental variable ``$PIP_LOG_FILE``. * Made the freeze command print installed packages to stdout instead of writing them to a file. Use simple redirection (e.g. ``pip freeze > stable-req.txt``) to get a file with requirements. * Fixed problem with freezing editable packages from a Git repository. * Added support for base URLs using ```` when parsing HTML pages. * Fixed installing of non-editable packages from version control systems. * Fixed issue with Bazaar's bzr+ssh scheme. * Added --download-dir option to the install command to retrieve package archives. If given an editable package it will create an archive of it. * Added ability to pass local file and directory paths to ``--find-links``, e.g. ``--find-links=file:///path/to/my/private/archive`` * Reduced the amount of console log messages when fetching a page to find a distribution was problematic. The full messages can be found in pip-log.txt. * Added ``--no-deps`` option to install ignore package dependencies * Added ``--no-index`` option to ignore the package index (PyPI) temporarily * Fixed installing editable packages from Git branches. * Fixes freezing of editable packages from Mercurial repositories. * Fixed handling read-only attributes of build files, e.g. of Subversion and Bazaar on Windows. * When downloading a file from a redirect, use the redirected location's extension to guess the compression (happens specifically when redirecting to a bitbucket.org tip.gz file). * Editable freeze URLs now always use revision hash/id rather than tip or branch names which could move. * Fixed comparison of repo URLs so incidental differences such as presence/absence of final slashes or quoted/unquoted special characters don't trigger "ignore/switch/wipe/backup" choice. * Fixed handling of attempt to checkout editable install to a non-empty, non-repo directory. 0.4 --- * Make ``-e`` work better with local hg repositories * Construct PyPI URLs the exact way easy_install constructs URLs (you might notice this if you use a custom index that is slash-sensitive). * Improvements on Windows (from `Ionel Maries Cristian `_). * Fixed problem with not being able to install private git repositories. * Make ``pip zip`` zip all its arguments, not just the first. * Fix some filename issues on Windows. * Allow the ``-i`` and ``--extra-index-url`` options in requirements files. * Fix the way bundle components are unpacked and moved around, to make bundles work. * Adds ``-s`` option to allow the access to the global site-packages if a virtualenv is to be created. * Fixed support for Subversion 1.6. 0.3.1 ----- * Improved virtualenv restart and various path/cleanup problems on win32. * Fixed a regression with installing from svn repositories (when not using ``-e``). * Fixes when installing editable packages that put their source in a subdirectory (like ``src/``). * Improve ``pip -h`` 0.3 --- * Added support for editable packages created from Git, Mercurial and Bazaar repositories and ability to freeze them. Refactored support for version control systems. * Do not use ``sys.exit()`` from inside the code, instead use a return. This will make it easier to invoke programmatically. * Put the install record in ``Package.egg-info/installed-files.txt`` (previously they went in ``site-packages/install-record-Package.txt``). * Fix a problem with ``pip freeze`` not including ``-e svn+`` when an svn structure is peculiar. * Allow ``pip -E`` to work with a virtualenv that uses a different version of Python than the parent environment. * Fixed Win32 virtualenv (``-E``) option. * Search the links passed in with ``-f`` for packages. * Detect zip files, even when the file doesn't have a ``.zip`` extension and it is served with the wrong Content-Type. * Installing editable from existing source now works, like ``pip install -e some/path/`` will install the package in ``some/path/``. Most importantly, anything that package requires will also be installed by pip. * Add a ``--path`` option to ``pip un/zip``, so you can avoid zipping files that are outside of where you expect. * Add ``--simulate`` option to ``pip zip``. 0.2.1 ----- * Fixed small problem that prevented using ``pip.py`` without actually installing pip. * Fixed ``--upgrade``, which would download and appear to install upgraded packages, but actually just reinstall the existing package. * Fixed Windows problem with putting the install record in the right place, and generating the ``pip`` script with Setuptools. * Download links that include embedded spaces or other unsafe characters (those characters get %-encoded). * Fixed use of URLs in requirement files, and problems with some blank lines. * Turn some tar file errors into warnings. 0.2 --- * Renamed to ``pip``, and to install you now do ``pip install PACKAGE`` * Added command ``pip zip PACKAGE`` and ``pip unzip PACKAGE``. This is particularly intended for Google App Engine to manage libraries to stay under the 1000-file limit. * Some fixes to bundles, especially editable packages and when creating a bundle using unnamed packages (like just an svn repository without ``#egg=Package``). 0.1.4 ----- * Added an option ``--install-option`` to pass options to pass arguments to ``setup.py install`` * ``.svn/`` directories are no longer included in bundles, as these directories are specific to a version of svn -- if you build a bundle on a system with svn 1.5, you can't use the checkout on a system with svn 1.4. Instead a file ``svn-checkout.txt`` is included that notes the original location and revision, and the command you can use to turn it back into an svn checkout. (Probably unpacking the bundle should, maybe optionally, recreate this information -- but that is not currently implemented, and it would require network access.) * Avoid ambiguities over project name case, where for instance MyPackage and mypackage would be considered different packages. This in particular caused problems on Macs, where ``MyPackage/`` and ``mypackage/`` are the same directory. * Added support for an environmental variable ``$PIP_DOWNLOAD_CACHE`` which will cache package downloads, so future installations won't require large downloads. Network access is still required, but just some downloads will be avoided when using this. 0.1.3 ----- * Always use ``svn checkout`` (not ``export``) so that ``tag_svn_revision`` settings give the revision of the package. * Don't update checkouts that came from ``.pybundle`` files. 0.1.2 ----- * Improve error text when there are errors fetching HTML pages when seeking packages. * Improve bundles: include empty directories, make them work with editable packages. * If you use ``-E env`` and the environment ``env/`` doesn't exist, a new virtual environment will be created. * Fix ``dependency_links`` for finding packages. 0.1.1 ----- * Fixed a NameError exception when running pip outside of a virtualenv environment. * Added HTTP proxy support (from Prabhu Ramachandran) * Fixed use of ``hashlib.md5`` on python2.5+ (also from Prabhu Ramachandran) 0.1 --- * Initial release Keywords: easy_install distutils setuptools egg virtualenv Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Build Tools Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO/SOURCES.txt000066400000000000000000000017151256046442300312730ustar00rootroot00000000000000AUTHORS.txt LICENSE.txt MANIFEST.in setup.cfg setup.py docs/ci-server-step-by-step.txt docs/configuration.txt docs/contributing.txt docs/glossary.txt docs/index.txt docs/installing.txt docs/news.txt docs/other-tools.txt docs/requirements.txt docs/usage.txt pip/__init__.py pip/_pkgutil.py pip/backwardcompat.py pip/basecommand.py pip/baseparser.py pip/download.py pip/exceptions.py pip/index.py pip/locations.py pip/log.py pip/req.py pip/runner.py pip/status_codes.py pip/util.py pip.egg-info/PKG-INFO pip.egg-info/SOURCES.txt pip.egg-info/dependency_links.txt pip.egg-info/entry_points.txt pip.egg-info/not-zip-safe pip.egg-info/top_level.txt pip/commands/__init__.py pip/commands/bundle.py pip/commands/completion.py pip/commands/freeze.py pip/commands/help.py pip/commands/install.py pip/commands/search.py pip/commands/uninstall.py pip/commands/unzip.py pip/commands/zip.py pip/vcs/__init__.py pip/vcs/bazaar.py pip/vcs/git.py pip/vcs/mercurial.py pip/vcs/subversion.pydependency_links.txt000066400000000000000000000000011256046442300333720ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO entry_points.txt000066400000000000000000000000651256046442300326230ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO[console_scripts] pip = pip:main pip-2.7 = pip:main circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO/not-zip-safe000066400000000000000000000000011256046442300316310ustar00rootroot00000000000000 circus-0.12.1/circus/tests/venv/lib/python3.3/site-packages/pip-7.7-py3.3.egg/EGG-INFO/top_level.txt000066400000000000000000000000041256046442300321270ustar00rootroot00000000000000pip circus-0.12.1/circus/tests/venv/lib/python3.4/000077500000000000000000000000001256046442300210155ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/no-global-site-packages.txt000066400000000000000000000000001256046442300261340ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/orig-prefix.txt000066400000000000000000000000621256046442300240070ustar00rootroot00000000000000/Library/Frameworks/Python.framework/Versions/3.4 circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/000077500000000000000000000000001256046442300235355ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/easy-install.pth000066400000000000000000000003151256046442300266560ustar00rootroot00000000000000import sys; sys.__plen = len(sys.path) ./pip-7.7-py3.4.egg import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new) circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/000077500000000000000000000000001256046442300262525ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO/000077500000000000000000000000001256046442300274055ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO/PKG-INFO000066400000000000000000000624131256046442300305100ustar00rootroot00000000000000Metadata-Version: 1.0 Name: pip Version: 1.1 Summary: pip installs packages. Python packages. An easy_install replacement Home-page: http://www.pip-installer.org Author: The pip developers Author-email: python-virtualenv@groups.google.com License: MIT Description: pip === `pip` is a tool for installing and managing Python packages, such as those found in the `Python Package Index`_. It's a replacement for easy_install_. :: $ pip install simplejson [... progress report ...] Successfully installed simplejson .. _`Python Package Index`: http://pypi.python.org/pypi .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall Upgrading a package:: $ pip install --upgrade simplejson [... progress report ...] Successfully installed simplejson Removing a package:: $ pip uninstall simplejson Uninstalling simplejson: /home/me/env/lib/python2.7/site-packages/simplejson /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info Proceed (y/n)? y Successfully uninstalled simplejson .. comment: The main website for pip is `www.pip-installer.org `_. You can also install the `in-development version `_ of pip with ``easy_install pip==dev``. Community --------- The homepage for pip is at `pip-installer.org `_. Bugs can be filed in the `pip issue tracker `_. Discussion happens on the `virtualenv email group `_. ==== News ==== Changelog ========= Next release (1.2) schedule --------------------------- Beta and final releases planned for the second half of 2012. 1.1 (2012-02-16) ---------------- * Fixed issue #326 - don't crash when a package's setup.py emits UTF-8 and then fails. Thanks Marc Abramowitz. * Added ``--target`` option for installing directly to arbitrary directory. Thanks Stavros Korokithakis. * Added support for authentication with Subversion repositories. Thanks Qiangning Hong. * Fixed issue #315 - ``--download`` now downloads dependencies as well. Thanks Qiangning Hong. * Errors from subprocesses will display the current working directory. Thanks Antti Kaihola. * Fixed issue #369 - compatibility with Subversion 1.7. Thanks Qiangning Hong. Note that setuptools remains incompatible with Subversion 1.7; to get the benefits of pip's support you must use Distribute rather than setuptools. * Fixed issue #57 - ignore py2app-generated OS X mpkg zip files in finder. Thanks Rene Dudfield. * Fixed issue #182 - log to ~/Library/Logs/ by default on OS X framework installs. Thanks Dan Callahan for report and patch. * Fixed issue #310 - understand version tags without minor version ("py3") in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for patch. * Fixed issue #7 - Pip now supports optionally installing setuptools "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker and Olivier Girardot. * Fixed issue #391 - freeze no longer borks on requirements files with --index-url or --find-links. Thanks Herbert Pfennig. * Fixed issue #288 - handle symlinks properly. Thanks lebedov for the patch. * Fixed issue #49 - pip install -U no longer reinstalls the same versions of packages. Thanks iguananaut for the pull request. * Removed ``-E`` option and ``PIP_RESPECT_VIRTUALENV``; both use a restart-in-venv mechanism that's broken, and neither one is useful since every virtualenv now has pip inside it. * Fixed issue #366 - pip throws IndexError when it calls `scraped_rel_links` * Fixed issue #22 - pip search should set and return a userful shell status code * Fixed issue #351 and #365 - added global ``--exists-action`` command line option to easier script file exists conflicts, e.g. from editable requirements from VCS that have a changed repo URL. 1.0.2 (2011-07-16) ------------------ * Fixed docs issues. * Fixed issue #295 - Reinstall a package when using the ``install -I`` option * Fixed issue #283 - Finds a Git tag pointing to same commit as origin/master * Fixed issue #279 - Use absolute path for path to docs in setup.py * Fixed issue #314 - Correctly handle exceptions on Python3. * Fixed issue #320 - Correctly parse ``--editable`` lines in requirements files 1.0.1 (2011-04-30) ------------------ * Start to use git-flow. * Fixed issue #274 - `find_command` should not raise AttributeError * Fixed issue #273 - respect Content-Disposition header. Thanks Bradley Ayers. * Fixed issue #233 - pathext handling on Windows. * Fixed issue #252 - svn+svn protocol. * Fixed issue #44 - multiple CLI searches. * Fixed issue #266 - current working directory when running setup.py clean. 1.0 (2011-04-04) ---------------- * Added Python 3 support! Huge thanks to Vinay Sajip, Vitaly Babiy, Kelsey Hightower, and Alex Gronholm, among others. * Download progress only shown on a real TTY. Thanks Alex Morega. * Fixed finding of VCS binaries to not be fooled by same-named directories. Thanks Alex Morega. * Fixed uninstall of packages from system Python for users of Debian/Ubuntu python-setuptools package (workaround until fixed in Debian and Ubuntu). * Added `get-pip.py `_ installer. Simply download and execute it, using the Python interpreter of your choice:: $ curl -O https://raw.github.com/pypa/pip/master/contrib/get-pip.py $ python get-pip.py This may have to be run as root. .. note:: Make sure you have `distribute `_ installed before using the installer! 0.8.3 ----- * Moved main repository to Github: https://github.com/pypa/pip * Transferred primary maintenance from Ian to Jannis Leidel, Carl Meyer, Brian Rosner * Fixed issue #14 - No uninstall-on-upgrade with URL package. Thanks Oliver Tonnhofer * Fixed issue #163 - Egg name not properly resolved. Thanks Igor Sobreira * Fixed issue #178 - Non-alphabetical installation of requirements. Thanks Igor Sobreira * Fixed issue #199 - Documentation mentions --index instead of --index-url. Thanks Kelsey Hightower * Fixed issue #204 - rmtree undefined in mercurial.py. Thanks Kelsey Hightower * Fixed bug in Git vcs backend that would break during reinstallation. * Fixed bug in Mercurial vcs backend related to pip freeze and branch/tag resolution. * Fixed bug in version string parsing related to the suffix "-dev". 0.8.2 ----- * Avoid redundant unpacking of bundles (from pwaller) * Fixed issue #32, #150, #161 - Fixed checking out the correct tag/branch/commit when updating an editable Git requirement. * Fixed issue #49 - Added ability to install version control requirements without making them editable, e.g.:: pip install git+https://github.com/pypa/pip/ * Fixed issue #175 - Correctly locate build and source directory on Mac OS X. * Added ``git+https://`` scheme to Git VCS backend. 0.8.1 ----- * Added global --user flag as shortcut for --install-option="--user". From Ronny Pfannschmidt. * Added support for `PyPI mirrors `_ as defined in `PEP 381 `_, from Jannis Leidel. * Fixed issue #138 - Git revisions ignored. Thanks John-Scott Atlakson. * Fixed issue #95 - Initial editable install of github package from a tag fails. Thanks John-Scott Atlakson. * Fixed issue #107 - Can't install if a directory in cwd has the same name as the package you're installing. * Fixed issue #39 - --install-option="--prefix=~/.local" ignored with -e. Thanks Ronny Pfannschmidt and Wil Tan. 0.8 --- * Track which ``build/`` directories pip creates, never remove directories it doesn't create. From Hugo Lopes Tavares. * Pip now accepts file:// index URLs. Thanks Dave Abrahams. * Various cleanup to make test-running more consistent and less fragile. Thanks Dave Abrahams. * Real Windows support (with passing tests). Thanks Dave Abrahams. * ``pip-2.7`` etc. scripts are created (Python-version specific scripts) * ``contrib/build-standalone`` script creates a runnable ``.zip`` form of pip, from Jannis Leidel * Editable git repos are updated when reinstalled * Fix problem with ``--editable`` when multiple ``.egg-info/`` directories are found. * A number of VCS-related fixes for ``pip freeze``, from Hugo Lopes Tavares. * Significant test framework changes, from Hugo Lopes Tavares. 0.7.2 ----- * Set zip_safe=False to avoid problems some people are encountering where pip is installed as a zip file. 0.7.1 ----- * Fixed opening of logfile with no directory name. Thanks Alexandre Conrad. * Temporary files are consistently cleaned up, especially after installing bundles, also from Alex Conrad. * Tests now require at least ScriptTest 1.0.3. 0.7 --- * Fixed uninstallation on Windows * Added ``pip search`` command. * Tab-complete names of installed distributions for ``pip uninstall``. * Support tab-completion when there is a global-option before the subcommand. * Install header files in standard (scheme-default) location when installing outside a virtualenv. Install them to a slightly more consistent non-standard location inside a virtualenv (since the standard location is a non-writable symlink to the global location). * pip now logs to a central location by default (instead of creating ``pip-log.txt`` all over the place) and constantly overwrites the file in question. On Unix and Mac OS X this is ``'$HOME/.pip/pip.log'`` and on Windows it's ``'%HOME%\\pip\\pip.log'``. You are still able to override this location with the ``$PIP_LOG_FILE`` environment variable. For a complete (appended) logfile use the separate ``'--log'`` command line option. * Fixed an issue with Git that left an editable packge as a checkout of a remote branch, even if the default behaviour would have been fine, too. * Fixed installing from a Git tag with older versions of Git. * Expand "~" in logfile and download cache paths. * Speed up installing from Mercurial repositories by cloning without updating the working copy multiple times. * Fixed installing directly from directories (e.g. ``pip install path/to/dir/``). * Fixed installing editable packages with ``svn+ssh`` URLs. * Don't print unwanted debug information when running the freeze command. * Create log file directory automatically. Thanks Alexandre Conrad. * Make test suite easier to run successfully. Thanks Dave Abrahams. * Fixed "pip install ." and "pip install .."; better error for directory without setup.py. Thanks Alexandre Conrad. * Support Debian/Ubuntu "dist-packages" in zip command. Thanks duckx. * Fix relative --src folder. Thanks Simon Cross. * Handle missing VCS with an error message. Thanks Alexandre Conrad. * Added --no-download option to install; pairs with --no-install to separate download and installation into two steps. Thanks Simon Cross. * Fix uninstalling from requirements file containing -f, -i, or --extra-index-url. * Leftover build directories are now removed. Thanks Alexandre Conrad. 0.6.3 ----- * Fixed import error on Windows with regard to the backwards compatibility package 0.6.2 ----- * Fixed uninstall when /tmp is on a different filesystem. * Fixed uninstallation of distributions with namespace packages. 0.6.1 ----- * Added support for the ``https`` and ``http-static`` schemes to the Mercurial and ``ftp`` scheme to the Bazaar backend. * Fixed uninstallation of scripts installed with easy_install. * Fixed an issue in the package finder that could result in an infinite loop while looking for links. * Fixed issue with ``pip bundle`` and local files (which weren't being copied into the bundle), from Whit Morriss. 0.6 --- * Add ``pip uninstall`` and uninstall-before upgrade (from Carl Meyer). * Extended configurability with config files and environment variables. * Allow packages to be upgraded, e.g., ``pip install Package==0.1`` then ``pip install Package==0.2``. * Allow installing/upgrading to Package==dev (fix "Source version does not match target version" errors). * Added command and option completion for bash and zsh. * Extended integration with virtualenv by providing an option to automatically use an active virtualenv and an option to warn if no active virtualenv is found. * Fixed a bug with pip install --download and editable packages, where directories were being set with 0000 permissions, now defaults to 755. * Fixed uninstallation of easy_installed console_scripts. * Fixed uninstallation on Mac OS X Framework layout installs * Fixed bug preventing uninstall of editables with source outside venv. * Creates download cache directory if not existing. 0.5.1 ----- * Fixed a couple little bugs, with git and with extensions. 0.5 --- * Added ability to override the default log file name (``pip-log.txt``) with the environmental variable ``$PIP_LOG_FILE``. * Made the freeze command print installed packages to stdout instead of writing them to a file. Use simple redirection (e.g. ``pip freeze > stable-req.txt``) to get a file with requirements. * Fixed problem with freezing editable packages from a Git repository. * Added support for base URLs using ```` when parsing HTML pages. * Fixed installing of non-editable packages from version control systems. * Fixed issue with Bazaar's bzr+ssh scheme. * Added --download-dir option to the install command to retrieve package archives. If given an editable package it will create an archive of it. * Added ability to pass local file and directory paths to ``--find-links``, e.g. ``--find-links=file:///path/to/my/private/archive`` * Reduced the amount of console log messages when fetching a page to find a distribution was problematic. The full messages can be found in pip-log.txt. * Added ``--no-deps`` option to install ignore package dependencies * Added ``--no-index`` option to ignore the package index (PyPI) temporarily * Fixed installing editable packages from Git branches. * Fixes freezing of editable packages from Mercurial repositories. * Fixed handling read-only attributes of build files, e.g. of Subversion and Bazaar on Windows. * When downloading a file from a redirect, use the redirected location's extension to guess the compression (happens specifically when redirecting to a bitbucket.org tip.gz file). * Editable freeze URLs now always use revision hash/id rather than tip or branch names which could move. * Fixed comparison of repo URLs so incidental differences such as presence/absence of final slashes or quoted/unquoted special characters don't trigger "ignore/switch/wipe/backup" choice. * Fixed handling of attempt to checkout editable install to a non-empty, non-repo directory. 0.4 --- * Make ``-e`` work better with local hg repositories * Construct PyPI URLs the exact way easy_install constructs URLs (you might notice this if you use a custom index that is slash-sensitive). * Improvements on Windows (from `Ionel Maries Cristian `_). * Fixed problem with not being able to install private git repositories. * Make ``pip zip`` zip all its arguments, not just the first. * Fix some filename issues on Windows. * Allow the ``-i`` and ``--extra-index-url`` options in requirements files. * Fix the way bundle components are unpacked and moved around, to make bundles work. * Adds ``-s`` option to allow the access to the global site-packages if a virtualenv is to be created. * Fixed support for Subversion 1.6. 0.3.1 ----- * Improved virtualenv restart and various path/cleanup problems on win32. * Fixed a regression with installing from svn repositories (when not using ``-e``). * Fixes when installing editable packages that put their source in a subdirectory (like ``src/``). * Improve ``pip -h`` 0.3 --- * Added support for editable packages created from Git, Mercurial and Bazaar repositories and ability to freeze them. Refactored support for version control systems. * Do not use ``sys.exit()`` from inside the code, instead use a return. This will make it easier to invoke programmatically. * Put the install record in ``Package.egg-info/installed-files.txt`` (previously they went in ``site-packages/install-record-Package.txt``). * Fix a problem with ``pip freeze`` not including ``-e svn+`` when an svn structure is peculiar. * Allow ``pip -E`` to work with a virtualenv that uses a different version of Python than the parent environment. * Fixed Win32 virtualenv (``-E``) option. * Search the links passed in with ``-f`` for packages. * Detect zip files, even when the file doesn't have a ``.zip`` extension and it is served with the wrong Content-Type. * Installing editable from existing source now works, like ``pip install -e some/path/`` will install the package in ``some/path/``. Most importantly, anything that package requires will also be installed by pip. * Add a ``--path`` option to ``pip un/zip``, so you can avoid zipping files that are outside of where you expect. * Add ``--simulate`` option to ``pip zip``. 0.2.1 ----- * Fixed small problem that prevented using ``pip.py`` without actually installing pip. * Fixed ``--upgrade``, which would download and appear to install upgraded packages, but actually just reinstall the existing package. * Fixed Windows problem with putting the install record in the right place, and generating the ``pip`` script with Setuptools. * Download links that include embedded spaces or other unsafe characters (those characters get %-encoded). * Fixed use of URLs in requirement files, and problems with some blank lines. * Turn some tar file errors into warnings. 0.2 --- * Renamed to ``pip``, and to install you now do ``pip install PACKAGE`` * Added command ``pip zip PACKAGE`` and ``pip unzip PACKAGE``. This is particularly intended for Google App Engine to manage libraries to stay under the 1000-file limit. * Some fixes to bundles, especially editable packages and when creating a bundle using unnamed packages (like just an svn repository without ``#egg=Package``). 0.1.4 ----- * Added an option ``--install-option`` to pass options to pass arguments to ``setup.py install`` * ``.svn/`` directories are no longer included in bundles, as these directories are specific to a version of svn -- if you build a bundle on a system with svn 1.5, you can't use the checkout on a system with svn 1.4. Instead a file ``svn-checkout.txt`` is included that notes the original location and revision, and the command you can use to turn it back into an svn checkout. (Probably unpacking the bundle should, maybe optionally, recreate this information -- but that is not currently implemented, and it would require network access.) * Avoid ambiguities over project name case, where for instance MyPackage and mypackage would be considered different packages. This in particular caused problems on Macs, where ``MyPackage/`` and ``mypackage/`` are the same directory. * Added support for an environmental variable ``$PIP_DOWNLOAD_CACHE`` which will cache package downloads, so future installations won't require large downloads. Network access is still required, but just some downloads will be avoided when using this. 0.1.3 ----- * Always use ``svn checkout`` (not ``export``) so that ``tag_svn_revision`` settings give the revision of the package. * Don't update checkouts that came from ``.pybundle`` files. 0.1.2 ----- * Improve error text when there are errors fetching HTML pages when seeking packages. * Improve bundles: include empty directories, make them work with editable packages. * If you use ``-E env`` and the environment ``env/`` doesn't exist, a new virtual environment will be created. * Fix ``dependency_links`` for finding packages. 0.1.1 ----- * Fixed a NameError exception when running pip outside of a virtualenv environment. * Added HTTP proxy support (from Prabhu Ramachandran) * Fixed use of ``hashlib.md5`` on python2.5+ (also from Prabhu Ramachandran) 0.1 --- * Initial release Keywords: easy_install distutils setuptools egg virtualenv Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Software Development :: Build Tools Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.4 Classifier: Programming Language :: Python :: 2.5 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.1 Classifier: Programming Language :: Python :: 3.2 circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO/SOURCES.txt000066400000000000000000000017151256046442300312750ustar00rootroot00000000000000AUTHORS.txt LICENSE.txt MANIFEST.in setup.cfg setup.py docs/ci-server-step-by-step.txt docs/configuration.txt docs/contributing.txt docs/glossary.txt docs/index.txt docs/installing.txt docs/news.txt docs/other-tools.txt docs/requirements.txt docs/usage.txt pip/__init__.py pip/_pkgutil.py pip/backwardcompat.py pip/basecommand.py pip/baseparser.py pip/download.py pip/exceptions.py pip/index.py pip/locations.py pip/log.py pip/req.py pip/runner.py pip/status_codes.py pip/util.py pip.egg-info/PKG-INFO pip.egg-info/SOURCES.txt pip.egg-info/dependency_links.txt pip.egg-info/entry_points.txt pip.egg-info/not-zip-safe pip.egg-info/top_level.txt pip/commands/__init__.py pip/commands/bundle.py pip/commands/completion.py pip/commands/freeze.py pip/commands/help.py pip/commands/install.py pip/commands/search.py pip/commands/uninstall.py pip/commands/unzip.py pip/commands/zip.py pip/vcs/__init__.py pip/vcs/bazaar.py pip/vcs/git.py pip/vcs/mercurial.py pip/vcs/subversion.pydependency_links.txt000066400000000000000000000000011256046442300333740ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO entry_points.txt000066400000000000000000000000651256046442300326250ustar00rootroot00000000000000circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO[console_scripts] pip = pip:main pip-2.7 = pip:main circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO/not-zip-safe000066400000000000000000000000011256046442300316330ustar00rootroot00000000000000 circus-0.12.1/circus/tests/venv/lib/python3.4/site-packages/pip-7.7-py3.4.egg/EGG-INFO/top_level.txt000066400000000000000000000000041256046442300321310ustar00rootroot00000000000000pip circus-0.12.1/circus/util.py000066400000000000000000001035071256046442300157160ustar00rootroot00000000000000import functools import logging import logging.config import os import re import shlex import socket import sys import time import traceback import json import struct try: import yaml except ImportError: yaml = None # NOQA try: import papa except ImportError: papa = None # NOQA try: import pwd import grp import fcntl except ImportError: fcntl = None grp = None pwd = None from tornado.ioloop import IOLoop from tornado import gen from tornado import concurrent from circus.py3compat import ( integer_types, bytestring, raise_with_tb, text_type ) try: from configparser import ( ConfigParser, MissingSectionHeaderError, ParsingError, DEFAULTSECT ) except ImportError: from ConfigParser import ( # NOQA ConfigParser, MissingSectionHeaderError, ParsingError, DEFAULTSECT ) try: from urllib.parse import urlparse except ImportError: from urlparse import urlparse # NOQA from datetime import timedelta from functools import wraps import signal from pipes import quote as shell_escape_arg try: import importlib reload_module = importlib.reload except (ImportError, AttributeError): from imp import reload as reload_module from zmq import ssh from psutil import AccessDenied, NoSuchProcess, Process from circus.exc import ConflictError from circus import logger from circus.py3compat import string_types # default endpoints DEFAULT_ENDPOINT_DEALER = "tcp://127.0.0.1:5555" DEFAULT_ENDPOINT_SUB = "tcp://127.0.0.1:5556" DEFAULT_ENDPOINT_STATS = "tcp://127.0.0.1:5557" DEFAULT_ENDPOINT_MULTICAST = "udp://237.219.251.97:12027" try: from setproctitle import setproctitle def _setproctitle(title): # NOQA setproctitle(title) except ImportError: def _setproctitle(title): # NOQA return MAXFD = 1024 if hasattr(os, "devnull"): REDIRECT_TO = os.devnull # PRAGMA: NOCOVER else: REDIRECT_TO = "/dev/null" # PRAGMA: NOCOVER LOG_LEVELS = { "critical": logging.CRITICAL, "error": logging.ERROR, "warning": logging.WARNING, "info": logging.INFO, "debug": logging.DEBUG} LOG_FMT = r"%(asctime)s %(name)s[%(process)d] [%(levelname)s] %(message)s" LOG_DATE_FMT = r"%Y-%m-%d %H:%M:%S" LOG_DATE_SYSLOG_FMT = r"%b %d %H:%M:%S" _SYMBOLS = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') _all_signals = {} IS_WINDOWS = os.name == 'nt' def get_working_dir(): """Returns current path, try to use PWD env first. Since os.getcwd() resolves symlinks, we want to use PWD first if present. """ pwd_ = os.environ.get('PWD') cwd = os.getcwd() if pwd_ is None: return cwd # if pwd is the same physical file than the one # pointed by os.getcwd(), we use it. try: pwd_stat = os.stat(pwd_) cwd_stat = os.stat(cwd) if pwd_stat.ino == cwd_stat.ino and pwd_stat.dev == cwd_stat.dev: return pwd_ except Exception: pass # otherwise, just use os.getcwd() return cwd def bytes2human(n): """Translates bytes into a human repr. """ if not isinstance(n, integer_types): raise TypeError(n) prefix = {} for i, s in enumerate(_SYMBOLS): prefix[s] = 1 << (i + 1) * 10 for s in reversed(_SYMBOLS): if n >= prefix[s]: value = int(float(n) / prefix[s]) return '%s%s' % (value, s) return "%sB" % n _HSYMBOLS = { 'customary': ('B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'), 'customary_ext': ('byte', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zetta', 'iotta'), 'iec': ('Bi', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), 'iec_ext': ('byte', 'kibi', 'mebi', 'gibi', 'tebi', 'pebi', 'exbi', 'zebi', 'yobi'), } _HSYMBOLS_VALUES = _HSYMBOLS.values() def human2bytes(s): init = s num = "" while s and s[0:1].isdigit() or s[0:1] == '.': num += s[0] s = s[1:] num = float(num) letter = s.strip() for sset in _HSYMBOLS_VALUES: if letter in sset: break else: if letter == 'k': # treat 'k' as an alias for 'K' as per: http://goo.gl/kTQMs sset = _HSYMBOLS['customary'] letter = letter.upper() else: raise ValueError("can't interpret %r" % init) prefix = {sset[0]: 1} for i, s in enumerate(sset[1:]): prefix[s] = 1 << (i+1) * 10 return int(num * prefix[letter]) # XXX weak dict ? _PROCS = {} def get_info(process=None, interval=0, with_childs=False): """Return information about a process. (can be an pid or a Process object) If process is None, will return the information about the current process. """ # XXX moce get_info to circus.process ? from circus.process import (get_children, get_memory_info, get_cpu_percent, get_memory_percent, get_cpu_times, get_nice, get_cmdline, get_create_time, get_username) if process is None or isinstance(process, int): if process is None: pid = os.getpid() else: pid = process if pid in _PROCS: process = _PROCS[pid] else: _PROCS[pid] = process = Process(pid) info = {} try: mem_info = get_memory_info(process) info['mem_info1'] = bytes2human(mem_info[0]) info['mem_info2'] = bytes2human(mem_info[1]) except AccessDenied: info['mem_info1'] = info['mem_info2'] = "N/A" try: info['cpu'] = get_cpu_percent(process, interval=interval) except AccessDenied: info['cpu'] = "N/A" try: info['mem'] = round(get_memory_percent(process), 1) except AccessDenied: info['mem'] = "N/A" try: cpu_times = get_cpu_times(process) ctime = timedelta(seconds=sum(cpu_times)) ctime = "%s:%s.%s" % (ctime.seconds // 60 % 60, str((ctime.seconds % 60)).zfill(2), str(ctime.microseconds)[:2]) except AccessDenied: ctime = "N/A" info['ctime'] = ctime try: info['pid'] = process.pid except AccessDenied: info['pid'] = 'N/A' try: info['username'] = get_username(process) except AccessDenied: info['username'] = 'N/A' try: info['nice'] = get_nice(process) except AccessDenied: info['nice'] = 'N/A' except NoSuchProcess: info['nice'] = 'Zombie' raw_cmdline = get_cmdline(process) try: cmdline = os.path.basename( shlex.split(raw_cmdline[0], posix=not IS_WINDOWS)[0] ) except (AccessDenied, IndexError): cmdline = "N/A" try: info['create_time'] = get_create_time(process) except AccessDenied: info['create_time'] = 'N/A' try: info['age'] = time.time() - get_create_time(process) except TypeError: info['create_time'] = get_create_time(process) except AccessDenied: info['age'] = 'N/A' info['cmdline'] = cmdline info['children'] = [] if with_childs: for child in get_children(process): info['children'].append(get_info(child, interval=interval)) return info TRUTHY_STRINGS = ('yes', 'true', 'on', '1') FALSY_STRINGS = ('no', 'false', 'off', '0') def to_bool(s): if isinstance(s, bool): return s if s is None: return False if s.lower().strip() in TRUTHY_STRINGS: return True elif s.lower().strip() in FALSY_STRINGS: return False else: raise ValueError("%r is not a boolean" % s) def to_signum(signum): if not _all_signals: for name in dir(signal): if name.startswith('SIG'): value = getattr(signal, name) _all_signals[name[3:]] = value _all_signals[name] = value _all_signals[str(value)] = value _all_signals[value] = value try: if isinstance(signum, string_types): signum = signum.upper() return _all_signals[signum] except KeyError: raise ValueError('signal invalid') if pwd is None: def to_uid(name): raise RuntimeError("'to_uid' not available on this operating system") else: def to_uid(name): # NOQA """Return an uid, given a user name. If the name is an integer, make sure it's an existing uid. If the user name is unknown, raises a ValueError. """ try: name = int(name) except ValueError: pass if isinstance(name, int): try: pwd.getpwuid(name) return name except KeyError: raise ValueError("%r isn't a valid user id" % name) from circus.py3compat import string_types # circular import fix if not isinstance(name, string_types): raise TypeError(name) try: return pwd.getpwnam(name).pw_uid except KeyError: raise ValueError("%r isn't a valid user name" % name) if grp is None: def to_gid(name): raise RuntimeError("'to_gid' not available on this operating system") else: def to_gid(name): # NOQA """Return a gid, given a group name If the group name is unknown, raises a ValueError. """ try: name = int(name) except ValueError: pass if isinstance(name, int): try: grp.getgrgid(name) return name # getgrid may raises overflow error on mac/os x, # fixed in python2.7.5 # see http://bugs.python.org/issue17531 except (KeyError, OverflowError): raise ValueError("No such group: %r" % name) from circus.py3compat import string_types # circular import fix if not isinstance(name, string_types): raise TypeError(name) try: return grp.getgrnam(name).gr_gid except KeyError: raise ValueError("No such group: %r" % name) def get_username_from_uid(uid): """Return the username of a given uid.""" if isinstance(uid, int): return pwd.getpwuid(uid).pw_name return uid def get_default_gid(uid): """Return the default group of a specific user.""" if isinstance(uid, int): return pwd.getpwuid(uid).pw_gid return pwd.getpwnam(uid).pw_gid def parse_env_str(env_str): env = dict() for kvs in env_str.split(','): k, v = kvs.split('=') env[k.strip()] = v.strip() return parse_env_dict(env) def parse_env_dict(env): ret = dict() for k, v in env.items(): v = re.sub(r'\$([A-Z]+[A-Z0-9_]*)', replace_env, v) ret[k.strip()] = v.strip() return ret def replace_env(var): return os.getenv(var.group(1)) def env_to_str(env): if not env: return "" return ",".join(["%s=%s" % (k, v) for k, v in sorted(env.items(), key=lambda i: i[0])]) if fcntl is None: def close_on_exec(fd): raise RuntimeError( "'close_on_exec' not available on this operating system") else: def close_on_exec(fd): # NOQA flags = fcntl.fcntl(fd, fcntl.F_GETFD) flags |= fcntl.FD_CLOEXEC fcntl.fcntl(fd, fcntl.F_SETFD, flags) def get_python_version(): """Get a 3 element tuple with the python version""" return sys.version_info[:3] INDENTATION_LEVEL = 0 def debuglog(func): @wraps(func) def _log(self, *args, **kw): if os.environ.get('DEBUG') is None: return func(self, *args, **kw) from circus import logger cls = self.__class__.__name__ global INDENTATION_LEVEL func_name = func.func_name if hasattr(func, 'func_name')\ else func.__name__ logger.debug(" " * INDENTATION_LEVEL + "'%s.%s' starts" % (cls, func_name)) INDENTATION_LEVEL += 1 try: return func(self, *args, **kw) finally: INDENTATION_LEVEL -= 1 logger.debug(" " * INDENTATION_LEVEL + "'%s.%s' ends" % (cls, func_name)) return _log def convert_opt(key, val): """ get opt """ if key == "env": val = env_to_str(val) else: if val is None: val = "" else: val = str(val) return val # taken from werkzeug class ImportStringError(ImportError): """Provides information about a failed :func:`import_string` attempt.""" #: String in dotted notation that failed to be imported. import_name = None #: Wrapped exception. exception = None def __init__(self, import_name, exception): self.import_name = import_name self.exception = exception msg = ( 'import_string() failed for %r. Possible reasons are:\n\n' '- missing __init__.py in a package;\n' '- package or module path not included in sys.path;\n' '- duplicated package or module name taking precedence in ' 'sys.path;\n' '- missing module, class, function or variable;\n\n' 'Debugged import:\n\n%s\n\n' 'Original exception:\n\n%s: %s') name = '' tracked = [] for part in import_name.replace(':', '.').split('.'): name += (name and '.') + part imported = resolve_name(name, silent=True) if imported: tracked.append((name, getattr(imported, '__file__', None))) else: track = ['- %r found in %r.' % (n, i) for n, i in tracked] track.append('- %r not found.' % name) msg = msg % (import_name, '\n'.join(track), exception.__class__.__name__, str(exception)) break ImportError.__init__(self, msg) def __repr__(self): return '<%s(%r, %r)>' % (self.__class__.__name__, self.import_name, self.exception) def resolve_name(import_name, silent=False, reload=False): """Imports an object based on a string. This is useful if you want to use import paths as endpoints or something similar. An import path can be specified either in dotted notation (``xml.sax.saxutils.escape``) or with a colon as object delimiter (``xml.sax.saxutils:escape``). If `silent` is True the return value will be `None` if the import fails. :param import_name: the dotted name for the object to import. :param silent: if set to `True` import errors are ignored and `None` is returned instead. :param reload: if set to `True` modules that are already loaded will be reloaded :return: imported object """ # force the import name to automatically convert to strings import_name = bytestring(import_name) try: if ':' in import_name: module, obj = import_name.split(':', 1) elif '.' in import_name and import_name not in sys.modules: module, obj = import_name.rsplit('.', 1) else: module, obj = import_name, None # __import__ is not able to handle unicode strings in the fromlist mod = None # if the module is a package if reload and module in sys.modules: try: importlib.invalidate_caches() except Exception: pass try: mod = reload_module(sys.modules[module]) except Exception: pass if not mod: if not obj: return __import__(module) try: mod = __import__(module, None, None, [obj]) except ImportError: if ':' in import_name: raise return __import__(import_name) if not obj: return mod try: return getattr(mod, obj) except AttributeError: # support importing modules not yet set up by the parent module # (or package for that matter) if ':' in import_name: raise return __import__(import_name) except ImportError as e: if not silent: raise_with_tb(ImportStringError(import_name, e)) _SECTION_NAME = '\w\.\-' _PATTERN1 = r'\$\(%%s\.([%s]+)\)' % _SECTION_NAME _PATTERN2 = r'\(\(%%s\.([%s]+)\)\)' % _SECTION_NAME _CIRCUS_VAR = re.compile(_PATTERN1 % 'circus' + '|' + _PATTERN2 % 'circus', re.I) def replace_gnu_args(data, prefix='circus', **options): fmt_options = {} for key, value in options.items(): key = key.lower() if prefix is not None: key = '%s.%s' % (prefix, key) if isinstance(value, dict): for subkey, subvalue in value.items(): subkey = subkey.lower() subkey = '%s.%s' % (key, subkey) fmt_options[subkey] = subvalue else: fmt_options[key] = value if prefix is None: pattern = r'\$\(([%s]+)\)|\(\(([%s]+)\)\)' % (_SECTION_NAME, _SECTION_NAME) match = re.compile(pattern, re.I) elif prefix == 'circus': match = _CIRCUS_VAR else: match = re.compile(_PATTERN1 % prefix + '|' + _PATTERN2 % prefix, re.I) def _repl(matchobj): option = None for result in matchobj.groups(): if result is not None: option = result.lower() break if prefix is not None and not option.startswith(prefix): option = '%s.%s' % (prefix, option) if option in fmt_options: return str(fmt_options[option]) return matchobj.group() return match.sub(_repl, data) class ObjectDict(dict): def __getattr__(self, item): return self[item] def configure_logger(logger, level='INFO', output="-", loggerconfig=None, name=None): if loggerconfig is None or loggerconfig.lower().strip() == "default": root_logger = logging.getLogger() loglevel = LOG_LEVELS.get(level.lower(), logging.INFO) root_logger.setLevel(loglevel) datefmt = LOG_DATE_FMT if output in ("-", "stdout"): handler = logging.StreamHandler() elif output.startswith('syslog://'): # URLs are syslog://host[:port]?facility or syslog:///path?facility info = urlparse(output) facility = 'user' if info.query in logging.handlers.SysLogHandler.facility_names: facility = info.query if info.netloc: address = (info.netloc, info.port or 514) else: address = info.path datefmt = LOG_DATE_SYSLOG_FMT handler = logging.handlers.SysLogHandler( address=address, facility=facility) else: if not IS_WINDOWS: handler = logging.handlers.WatchedFileHandler(output) close_on_exec(handler.stream.fileno()) else: # WatchedFileHandler is not supported on Windows, # but a FileHandler should be a good drop-in replacement # as log files are locked handler = logging.FileHandler(output) formatter = logging.Formatter(fmt=LOG_FMT, datefmt=datefmt) handler.setFormatter(formatter) root_logger.handlers = [handler] else: loggerconfig = os.path.abspath(loggerconfig) if loggerconfig.lower().endswith(".ini"): logging.config.fileConfig(loggerconfig, disable_existing_loggers=True) elif loggerconfig.lower().endswith(".json"): if not hasattr(logging.config, "dictConfig"): raise Exception("Logger configuration file %s appears to be " "a JSON file but this version of Python " "does not support the " "logging.config.dictConfig function. Try " "Python 2.7.") with open(loggerconfig, "r") as fh: logging.config.dictConfig(json.loads(fh.read())) elif loggerconfig.lower().endswith(".yaml"): if not hasattr(logging.config, "dictConfig"): raise Exception("Logger configuration file %s appears to be " "a YAML file but this version of Python " "does not support the " "logging.config.dictConfig function. Try " "Python 2.7.") if yaml is None: raise Exception("Logger configuration file %s appears to be " "a YAML file but PyYAML is not available. " "Try: pip install PyYAML" % (shell_escape_arg(loggerconfig),)) with open(loggerconfig, "r") as fh: logging.config.dictConfig(yaml.load(fh.read())) else: raise Exception("Logger configuration file %s is not in one " "of the recognized formats. The file name " "should be: *.ini, *.json or *.yaml." % (shell_escape_arg(loggerconfig),)) class StrictConfigParser(ConfigParser): def _read(self, fp, fpname): cursect = None # None, or a dictionary optname = None lineno = 0 e = None # None, or an exception while True: line = fp.readline() if not line: break lineno += 1 # comment or blank line? if line.strip() == '' or line[0] in '#;': continue if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": # no leading whitespace continue # continuation line? if line[0].isspace() and cursect is not None and optname: value = line.strip() if value: cursect[optname].append(value) # a section header or option header? else: # is it a section header? mo = self.SECTCRE.match(line) if mo: sectname = mo.group('header') if sectname in self._sections: # we're extending/overriding, we're good cursect = self._sections[sectname] elif sectname == DEFAULTSECT: cursect = self._defaults else: cursect = self._dict() cursect['__name__'] = sectname self._sections[sectname] = cursect # So sections can't start with a continuation line optname = None # no section header in the file? elif cursect is None: raise MissingSectionHeaderError(fpname, lineno, line) # an option line? else: try: mo = self._optcre.match(line) # 2.7 except AttributeError: mo = self.OPTCRE.match(line) # 2.6 if mo: optname, vi, optval = mo.group('option', 'vi', 'value') self.optionxform = text_type optname = self.optionxform(optname.rstrip()) # We don't want to override. if optname in cursect: continue # This check is fine because the OPTCRE cannot # match if it would set optval to None if optval is not None: if vi in ('=', ':') and ';' in optval: # ';' is a comment delimiter only if it follows # a spacing character pos = optval.find(';') if pos != -1 and optval[pos - 1].isspace(): optval = optval[:pos] optval = optval.strip() # allow empty values if optval == '""': optval = '' cursect[optname] = [optval] else: # valueless option handling cursect[optname] = optval else: # a non-fatal parsing error occurred. set up the # exception but keep going. the exception will be # raised at the end of the file and will contain a # list of all bogus lines if not e: e = ParsingError(fpname) e.append(lineno, repr(line)) # if any parsing errors occurred, raise an exception if e: raise e # join the multi-line values collected while reading all_sections = [self._defaults] all_sections.extend(self._sections.values()) for options in all_sections: for name, val in options.items(): if isinstance(val, list): options[name] = '\n'.join(val) def get_connection(socket, endpoint, ssh_server=None, ssh_keyfile=None): if ssh_server is None: socket.connect(endpoint) else: try: try: ssh.tunnel_connection(socket, endpoint, ssh_server, keyfile=ssh_keyfile) except ImportError: ssh.tunnel_connection(socket, endpoint, ssh_server, keyfile=ssh_keyfile, paramiko=True) except ImportError: raise ImportError("pexpect was not found, and failed to use " "Paramiko. You need to install Paramiko") def load_virtualenv(watcher, py_ver=None): if not watcher.copy_env: raise ValueError('copy_env must be True to to use virtualenv') if not py_ver: py_ver = sys.version.split()[0][:3] # XXX Posix scheme - need to add others sitedir = os.path.join(watcher.virtualenv, 'lib', 'python' + py_ver, 'site-packages') if not os.path.exists(sitedir): raise ValueError("%s does not exist" % sitedir) bindir = os.path.join(watcher.virtualenv, 'bin') if os.path.exists(bindir): watcher.env['PATH'] = ':'.join([bindir, watcher.env.get('PATH', '')]) def process_pth(sitedir, name): packages = set() fullname = os.path.join(sitedir, name) try: f = open(fullname, "rU") except IOError: return with f: for line in f.readlines(): if line.startswith(("#", "import")): continue line = line.rstrip() pkg_path = os.path.abspath(os.path.join(sitedir, line)) if os.path.exists(pkg_path): packages.add(pkg_path) return packages venv_pkgs = set() dotpth = os.extsep + "pth" for name in os.listdir(sitedir): if name.endswith(dotpth): try: packages = process_pth(sitedir, name) if packages: venv_pkgs |= packages except OSError: continue py_path = watcher.env.get('PYTHONPATH') path = None if venv_pkgs: venv_path = os.pathsep.join(venv_pkgs) if py_path: path = os.pathsep.join([venv_path, py_path]) else: path = venv_path # Add watcher virtualenv site-packages dir to the python path if path and sitedir not in path.split(os.pathsep): path = os.pathsep.join([path, sitedir]) else: if py_path: path = os.pathsep.join([py_path, sitedir]) else: path = sitedir watcher.env['PYTHONPATH'] = path def create_udp_socket(mcast_addr, mcast_port): """Create an udp multicast socket for circusd cluster auto-discovery. mcast_addr must be between 224.0.0.0 and 239.255.255.255 """ try: ip_splitted = list(map(int, mcast_addr.split('.'))) mcast_port = int(mcast_port) except ValueError: raise ValueError('Wrong UDP multicast_endpoint configuration. Should ' 'looks like: "%r"' % DEFAULT_ENDPOINT_MULTICAST) if ip_splitted[0] < 224 or ip_splitted[0] > 239: raise ValueError('The multicast address is not valid should be ' 'between 224.0.0.0 and 239.255.255.255') any_addr = "0.0.0.0" sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # Allow reutilization of addr sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Some platform exposes SO_REUSEPORT if hasattr(socket, 'SO_REUSEPORT'): try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) except socket.error: # see #699 pass # Put packet ttl to max # The following ttl fix is to make this work on SunOS and BSD systems. # Ref : Issue #875 ttl = struct.pack('B', 255) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) # Register socket to multicast group sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(mcast_addr) + socket.inet_aton(any_addr)) # And finally bind all interfaces sock.bind((any_addr, mcast_port)) return sock # taken from http://stackoverflow.com/questions/1165352 class DictDiffer(object): """ Calculate the difference between two dictionaries as: (1) items added (2) items removed (3) keys same in both but changed values (4) keys same in both and unchanged values """ def __init__(self, current_dict, past_dict): self.current_dict, self.past_dict = current_dict, past_dict self.set_current, self.set_past = (set(current_dict.keys()), set(past_dict.keys())) self.intersect = self.set_current.intersection(self.set_past) def added(self): return self.set_current - self.intersect def removed(self): return self.set_past - self.intersect def changed(self): return set(o for o in self.intersect if self.past_dict[o] != self.current_dict[o]) def unchanged(self): return set(o for o in self.intersect if self.past_dict[o] == self.current_dict[o]) def dict_differ(dict1, dict2): return len(DictDiffer(dict1, dict2).changed()) > 0 def _synchronized_cb(arbiter, future): if arbiter is not None: arbiter._exclusive_running_command = None def synchronized(name): def real_decorator(f): @wraps(f) def wrapper(self, *args, **kwargs): arbiter = None if hasattr(self, "arbiter"): arbiter = self.arbiter elif hasattr(self, "_exclusive_running_command"): arbiter = self if arbiter is not None: if arbiter._restarting: raise ConflictError("arbiter is restarting...") if arbiter._exclusive_running_command is not None: raise ConflictError("arbiter is already running %s command" % arbiter._exclusive_running_command) arbiter._exclusive_running_command = name resp = None try: resp = f(self, *args, **kwargs) finally: if isinstance(resp, concurrent.Future): cb = functools.partial(_synchronized_cb, arbiter) resp.add_done_callback(cb) else: if arbiter is not None: arbiter._exclusive_running_command = None return resp return wrapper return real_decorator def tornado_sleep(duration): """Sleep without blocking the tornado event loop To use with a gen.coroutines decorated function Thanks to http://stackoverflow.com/a/11135204/433050 """ return gen.Task(IOLoop.instance().add_timeout, time.time() + duration) class TransformableFuture(concurrent.Future): _upstream_future = None _upstream_callback = None _result = None _exception = None def _transform_function(x): return x def set_transform_function(self, fn): self._transform_function = fn def set_upstream_future(self, upstream_future): self._upstream_future = upstream_future def result(self, timeout=None): if self._upstream_future is None: raise Exception("upstream_future is not set") return self._transform_function(self._result) def _internal_callback(self, future): self._result = future.result() self._exception = future.exception() if self._upstream_callback is not None: self._upstream_callback(self) def add_done_callback(self, fn): if self._upstream_future is None: raise Exception("upstream_future is not set") self._upstream_callback = fn self._upstream_future.add_done_callback(self._internal_callback) def exception(self, timeout=None): if self._exception: return self._exception else: return None def check_future_exception_and_log(future): if isinstance(future, concurrent.Future): exception = future.exception() if exception is not None: logger.error("exception %s caught" % exception) if hasattr(future, "exc_info"): exc_info = future.exc_info() traceback.print_tb(exc_info[2]) return exception circus-0.12.1/circus/watcher.py000066400000000000000000001245231256046442300163770ustar00rootroot00000000000000import copy import errno import os import signal import time import sys from random import randint try: from itertools import zip_longest as izip_longest except ImportError: # noinspection PyUnresolvedReferences from itertools import izip_longest # NOQA import site from tornado import gen from psutil import NoSuchProcess, TimeoutExpired import zmq.utils.jsonapi as json from zmq.eventloop import ioloop from circus.process import Process, DEAD_OR_ZOMBIE, UNEXISTING from circus.papa_process_proxy import PapaProcessProxy from circus import logger from circus import util from circus.stream import get_stream, Redirector from circus.stream.papa_redirector import PapaRedirector from circus.util import parse_env_dict, resolve_name, tornado_sleep, IS_WINDOWS from circus.util import papa from circus.py3compat import bytestring, is_callable, b, PY2 class Watcher(object): """ Class managing a list of processes for a given command. Options: - **name**: name given to the watcher. Used to uniquely identify it. - **cmd**: the command to run. May contain *$WID*, which will be replaced by **wid**. - **args**: the arguments for the command to run. Can be a list or a string. If **args** is a string, it's splitted using :func:`shlex.split`. Defaults to None. - **numprocesses**: Number of processes to run. - **working_dir**: the working directory to run the command in. If not provided, will default to the current working directory. - **shell**: if *True*, will run the command in the shell environment. *False* by default. **warning: this is a security hazard**. - **uid**: if given, is the user id or name the command should run with. The current uid is the default. - **gid**: if given, is the group id or name the command should run with. The current gid is the default. - **send_hup**: if True, a process reload will be done by sending the SIGHUP signal. Defaults to False. - **stop_signal**: the signal to send when stopping the process. Defaults to SIGTERM. - **stop_children**: send the **stop_signal** to the children too. Defaults to False. - **env**: a mapping containing the environment variables the command will run with. Optional. - **rlimits**: a mapping containing rlimit names and values that will be set before the command runs. - **stdout_stream**: a mapping that defines the stream for the process stdout. Defaults to None. Optional. When provided, *stdout_stream* is a mapping containing up to three keys: - **class**: the stream class. Defaults to `circus.stream.FileStream` - **filename**: the filename, if using a FileStream - **max_bytes**: maximum file size, after which a new output file is opened. defaults to 0 which means no maximum size (only applicable with FileStream). - **backup_count**: how many backups to retain when rotating files according to the max_bytes parameter. defaults to 0 which means no backups are made (only applicable with FileStream) This mapping will be used to create a stream callable of the specified class. Each entry received by the callable is a mapping containing: - **pid** - the process pid - **name** - the stream name (*stderr* or *stdout*) - **data** - the data This is not supported on Windows. - **stderr_stream**: a mapping that defines the stream for the process stderr. Defaults to None. Optional. When provided, *stderr_stream* is a mapping containing up to three keys: - **class**: the stream class. Defaults to `circus.stream.FileStream` - **filename**: the filename, if using a FileStream - **max_bytes**: maximum file size, after which a new output file is opened. defaults to 0 which means no maximum size (only applicable with FileStream) - **backup_count**: how many backups to retain when rotating files according to the max_bytes parameter. defaults to 0 which means no backups are made (only applicable with FileStream). This mapping will be used to create a stream callable of the specified class. Each entry received by the callable is a mapping containing: - **pid** - the process pid - **name** - the stream name (*stderr* or *stdout*) - **data** - the data This is not supported on Windows. - **priority** -- integer that defines a priority for the watcher. When the Arbiter do some operations on all watchers, it will sort them with this field, from the bigger number to the smallest. (default: 0) - **singleton** -- If True, this watcher has a single process. (default:False) - **use_sockets** -- If True, the processes will inherit the file descriptors, thus can reuse the sockets opened by circusd. (default: False) - **on_demand** -- If True, the processes will be started only at the first connection to the socket (default: False) - **copy_env** -- If True, the environment in which circus is running run will be reproduced for the workers. This defaults to True on Windows as you cannot run any executable without the **SYSTEMROOT** variable. (default: False) - **copy_path** -- If True, circusd *sys.path* is sent to the process through *PYTHONPATH*. You must activate **copy_env** for **copy_path** to work. (default: False) - **max_age**: If set after around max_age seconds, the process is replaced with a new one. (default: 0, Disabled) - **max_age_variance**: The maximum number of seconds that can be added to max_age. This extra value is to avoid restarting all processes at the same time. A process will live between max_age and max_age + max_age_variance seconds. - **hooks**: callback functions for hooking into the watcher startup and shutdown process. **hooks** is a dict where each key is the hook name and each value is a 2-tuple with the name of the callable or the callabled itself and a boolean flag indicating if an exception occuring in the hook should not be ignored. Possible values for the hook name: *before_start*, *after_start*, *before_spawn*, *after_spawn*, *before_stop*, *after_stop*., *before_signal*, *after_signal* or *extended_stats*. - **options** -- extra options for the worker. All options found in the configuration file for instance, are passed in this mapping -- this can be used by plugins for watcher-specific options. - **respawn** -- If set to False, the processes handled by a watcher will not be respawned automatically. (default: True) - **virtualenv** -- The root directory of a virtualenv. If provided, the watcher will load the environment for its execution. (default: None) - **close_child_stdout**: If True, closes the stdout after the fork. default: False. - **close_child_stderr**: If True, closes the stderr after the fork. default: False. - **use_papa**: If True, use the papa process kernel for this process. default: False. """ def __init__(self, name, cmd, args=None, numprocesses=1, warmup_delay=0., working_dir=None, shell=False, shell_args=None, uid=None, max_retry=5, gid=None, send_hup=False, stop_signal=signal.SIGTERM, stop_children=False, env=None, graceful_timeout=30.0, prereload_fn=None, rlimits=None, executable=None, stdout_stream=None, stderr_stream=None, priority=0, loop=None, singleton=False, use_sockets=False, copy_env=False, copy_path=False, max_age=0, max_age_variance=30, hooks=None, respawn=True, autostart=True, on_demand=False, virtualenv=None, close_child_stdout=False, close_child_stderr=False, virtualenv_py_ver=None, use_papa=False, **options): self.name = name self.use_sockets = use_sockets self.on_demand = on_demand self.res_name = name.lower().replace(" ", "_") self.numprocesses = int(numprocesses) self.warmup_delay = warmup_delay self.cmd = cmd self.args = args self._status = "stopped" self.graceful_timeout = float(graceful_timeout) self.prereload_fn = prereload_fn self.executable = None self.priority = priority self.stdout_stream_conf = copy.copy(stdout_stream) self.stderr_stream_conf = copy.copy(stderr_stream) self.stdout_stream = get_stream(self.stdout_stream_conf) self.stderr_stream = get_stream(self.stderr_stream_conf) self.stream_redirector = None self.max_retry = max_retry self._options = options self.singleton = singleton self.copy_env = copy_env self.copy_path = copy_path self.virtualenv = virtualenv self.virtualenv_py_ver = virtualenv_py_ver self.max_age = int(max_age) self.max_age_variance = int(max_age_variance) self.ignore_hook_failure = ['before_stop', 'after_stop', 'before_signal', 'after_signal', 'extended_stats'] self.respawn = respawn self.autostart = autostart self.close_child_stdout = close_child_stdout self.close_child_stderr = close_child_stderr self.use_papa = use_papa and papa is not None self.loop = loop or ioloop.IOLoop.instance() if singleton and self.numprocesses not in (0, 1): raise ValueError("Cannot have %d processes with a singleton " " watcher" % self.numprocesses) if IS_WINDOWS: if self.stdout_stream or self.stderr_stream: raise NotImplementedError("Streams are not supported" " on Windows.") if not copy_env and not env: # Copy the env by default on Windows as we can't run any # executable without some env variables # Eventually, we could set only some required variables, # such as SystemRoot self.copy_env = True self.optnames = (("numprocesses", "warmup_delay", "working_dir", "uid", "gid", "send_hup", "stop_signal", "stop_children", "shell", "shell_args", "env", "max_retry", "cmd", "args", "respawn", "graceful_timeout", "executable", "use_sockets", "priority", "copy_env", "singleton", "stdout_stream_conf", "on_demand", "stderr_stream_conf", "max_age", "max_age_variance", "close_child_stdout", "close_child_stderr", "use_papa") + tuple(options.keys())) if not working_dir: # working dir hasn't been set working_dir = util.get_working_dir() self.working_dir = working_dir self.processes = {} self.shell = shell self.shell_args = shell_args self.uid = uid self.gid = gid if self.copy_env: self.env = os.environ.copy() if self.copy_path: path = os.pathsep.join(sys.path) self.env['PYTHONPATH'] = path if env is not None: self.env.update(env) else: if self.copy_path: raise ValueError(('copy_env and copy_path must have the ' 'same value')) self.env = env if self.virtualenv: util.load_virtualenv(self, py_ver=virtualenv_py_ver) # load directories in PYTHONPATH if provided # so if a hook is there, it can be loaded if self.env is not None and 'PYTHONPATH' in self.env: for path in self.env['PYTHONPATH'].split(os.pathsep): if path in sys.path: continue site.addsitedir(path) self.rlimits = rlimits self.send_hup = send_hup self.stop_signal = stop_signal self.stop_children = stop_children self.sockets = self.evpub_socket = None self.arbiter = None self.hooks = {} self._resolve_hooks(hooks) self._found_wids = [] if self.use_papa: with papa.Papa() as p: base_name = 'circus.{0}.*'.format(name.lower()) running = p.list_processes(base_name) self._found_wids = [int(proc_name[len(base_name) - 1:]) for proc_name in running] def _reload_hook(self, key, hook, ignore_error): hook_name = key.split('.')[-1] self._resolve_hook(hook_name, hook, ignore_error, reload_module=True) @property def _redirector_class(self): return PapaRedirector if self.use_papa else Redirector @property def _process_class(self): return PapaProcessProxy if self.use_papa else Process def _reload_stream(self, key, val): parts = key.split('.', 1) stream_type = 'stdout' if parts[0] == 'stdout_stream' else 'stderr' old_stream = self.stream_redirector.get_stream(stream_type) if\ self.stream_redirector else None if stream_type == 'stdout': self.stdout_stream_conf[parts[1]] = val new_stream = get_stream(self.stdout_stream_conf, reload=True) self.stdout_stream = new_stream else: self.stderr_stream_conf[parts[1]] = val new_stream = get_stream(self.stderr_stream_conf, reload=True) self.stderr_stream = new_stream if self.stream_redirector: self.stream_redirector.change_stream(stream_type, new_stream) else: self.stream_redirector = self._redirector_class( self.stdout_stream, self.stderr_stream, loop=self.loop) if old_stream: if hasattr(old_stream, 'close'): old_stream.close() return 0 self.stream_redirector.start() return 1 def _create_redirectors(self): if self.stdout_stream or self.stderr_stream: if self.stream_redirector: self.stream_redirector.stop() self.stream_redirector = self._redirector_class( self.stdout_stream, self.stderr_stream, loop=self.loop) else: self.stream_redirector = None def _resolve_hook(self, name, callable_or_name, ignore_failure, reload_module=False): if is_callable(callable_or_name): self.hooks[name] = callable_or_name else: # will raise ImportError on failure self.hooks[name] = resolve_name(callable_or_name, reload=reload_module) if ignore_failure: self.ignore_hook_failure.append(name) def _resolve_hooks(self, hooks): """Check the supplied hooks argument to make sure we can find callables""" if hooks is None: return for name, (callable_or_name, ignore_failure) in hooks.items(): self._resolve_hook(name, callable_or_name, ignore_failure) @property def pending_socket_event(self): return self.on_demand and not self.arbiter.socket_event @classmethod def load_from_config(cls, config): if 'env' in config: config['env'] = parse_env_dict(config['env']) cfg = config.copy() w = cls(name=config.pop('name'), cmd=config.pop('cmd'), **config) w._cfg = cfg return w @util.debuglog def initialize(self, evpub_socket, sockets, arbiter): self.evpub_socket = evpub_socket self.sockets = sockets self.arbiter = arbiter def __len__(self): return len(self.processes) def notify_event(self, topic, msg): """Publish a message on the event publisher channel""" name = bytestring(self.res_name) multipart_msg = [b("watcher.%s.%s" % (name, topic)), json.dumps(msg)] if self.evpub_socket is not None and not self.evpub_socket.closed: self.evpub_socket.send_multipart(multipart_msg) @util.debuglog def reap_process(self, pid, status=None): """ensure that the process is killed (and not a zombie)""" if pid not in self.processes: return process = self.processes.pop(pid) timeout = 0.001 while status is None: if IS_WINDOWS: try: # On Windows we can't use waitpid as it's blocking, # so we use psutils' wait status = process.wait(timeout=timeout) except TimeoutExpired: continue else: try: _, status = os.waitpid(pid, os.WNOHANG) except OSError as e: if e.errno == errno.EAGAIN: time.sleep(timeout) continue elif e.errno == errno.ECHILD: status = None else: raise if status is None: # nothing to do here, we do not have any child # process running # but we still need to send the "reap" signal. # # This can happen if poll() or wait() were called on # the underlying process. logger.debug('reaping already dead process %s [%s]', pid, self.name) self.notify_event( "reap", {"process_pid": pid, "time": time.time(), "exit_code": process.returncode()}) process.stop() return # get return code if hasattr(os, 'WIFSIGNALED'): exit_code = 0 if os.WIFSIGNALED(status): # The Python Popen object returns <-signal> in it's returncode # property if the process exited on a signal, so emulate that # behavior here so that pubsub clients watching for reap can # distinguish between an exit with a non-zero exit code and # a signal'd exit. This is also consistent with the notify # event reap message above that uses the returncode function # (that ends up calling Popen.returncode) exit_code = -os.WTERMSIG(status) # process exited using exit(2) system call; return the # integer exit(2) system call has been called with elif os.WIFEXITED(status): exit_code = os.WEXITSTATUS(status) else: # should never happen raise RuntimeError("Unknown process exit status") else: # On Windows we don't have such distinction exit_code = status # if the process is dead or a zombie try to definitely stop it. if process.status in (DEAD_OR_ZOMBIE, UNEXISTING): process.stop() logger.debug('reaping process %s [%s]', pid, self.name) self.notify_event("reap", {"process_pid": pid, "time": time.time(), "exit_code": exit_code}) @util.debuglog def reap_processes(self): """Reap all the processes for this watcher. """ if self.is_stopped(): logger.debug('do not reap processes as the watcher is stopped') return # reap_process changes our dict, look through the copy of keys for pid in list(self.processes.keys()): self.reap_process(pid) @gen.coroutine @util.debuglog def manage_processes(self): """Manage processes.""" if self.is_stopped(): return # remove dead or zombie processes first for process in list(self.processes.values()): if process.status in (DEAD_OR_ZOMBIE, UNEXISTING): self.processes.pop(process.pid) if self.max_age: yield self.remove_expired_processes() # adding fresh processes if len(self.processes) < self.numprocesses and not self.is_stopping(): if self.respawn: yield self.spawn_processes() elif not len(self.processes) and not self.on_demand: yield self._stop() # removing extra processes if len(self.processes) > self.numprocesses: processes_to_kill = [] for process in sorted(self.processes.values(), key=lambda process: process.started, reverse=True)[self.numprocesses:]: if process.status in (DEAD_OR_ZOMBIE, UNEXISTING): self.processes.pop(process.pid) else: processes_to_kill.append(process) removes = yield [self.kill_process(process) for process in processes_to_kill] for i, process in enumerate(processes_to_kill): if removes[i]: self.processes.pop(process.pid) @gen.coroutine @util.debuglog def remove_expired_processes(self): expired_processes = [p for p in self.processes.values() if p.age() > (self.max_age + randint(0, self.max_age_variance))] removes = yield [self.kill_process(x) for x in expired_processes] for i, process in enumerate(expired_processes): if removes[i]: self.processes.pop(process.pid) @gen.coroutine @util.debuglog def reap_and_manage_processes(self): """Reap & manage processes.""" if self.is_stopped(): return self.reap_processes() yield self.manage_processes() @gen.coroutine @util.debuglog def spawn_processes(self): """Spawn processes. """ # when an on_demand process dies, do not restart it until # the next event if self.pending_socket_event: self._status = "stopped" return for i in self._found_wids: self.spawn_process(i) yield tornado_sleep(0) self._found_wids = {} for i in range(self.numprocesses - len(self.processes)): res = self.spawn_process() if res is False: yield self._stop() break delay = self.warmup_delay if isinstance(res, float): delay -= (time.time() - res) if delay < 0: delay = 0 yield tornado_sleep(delay) def _get_sockets_fds(self): # XXX should be cached if self.sockets is None: return {} return dict((name, sock.fileno()) for name, sock in self.sockets.items() if sock.use_papa == self.use_papa) def spawn_process(self, recovery_wid=None): """Spawn process. Return True if ok, False if the watcher must be stopped """ if self.is_stopped(): return True if not recovery_wid and not self.call_hook('before_spawn'): return False cmd = util.replace_gnu_args(self.cmd, env=self.env) nb_tries = 0 # start the redirector now so we can catch any startup errors if self.stream_redirector: self.stream_redirector.start() while nb_tries < self.max_retry or self.max_retry == -1: process = None pipe_stdout = self.stdout_stream is not None pipe_stderr = self.stderr_stream is not None # noinspection PyPep8Naming ProcCls = self._process_class try: process = ProcCls(self.name, recovery_wid or self._nextwid, cmd, args=self.args, working_dir=self.working_dir, shell=self.shell, uid=self.uid, gid=self.gid, env=self.env, rlimits=self.rlimits, executable=self.executable, use_fds=self.use_sockets, watcher=self, pipe_stdout=pipe_stdout, pipe_stderr=pipe_stderr, close_child_stdout=self.close_child_stdout, close_child_stderr=self.close_child_stderr) # stream stderr/stdout if configured if self.stream_redirector: self.stream_redirector.add_redirections(process) self.processes[process.pid] = process logger.debug('running %s process [pid %d]', self.name, process.pid) if not self.call_hook('after_spawn', pid=process.pid): self.kill_process(process) del self.processes[process.pid] return False # catch ValueError as well, as a misconfigured rlimit setting could # lead to bad infinite retries here except (OSError, ValueError) as e: logger.warning('error in %r: %s', self.name, str(e)) if process is None: nb_tries += 1 continue else: self.notify_event("spawn", {"process_pid": process.pid, "time": process.started}) return process.started return False @util.debuglog def send_signal_process(self, process, signum): """Send the signum signal to the process The signal is sent to the process itself then to all the children """ children = None try: # getting the process children children = process.children() # sending the signal to the process itself self.send_signal(process.pid, signum) self.notify_event("kill", {"process_pid": process.pid, "time": time.time()}) except NoSuchProcess: # already dead ! if children is None: return # now sending the same signal to all the children for child_pid in children: try: process.send_signal_child(child_pid, signum) self.notify_event("kill", {"process_pid": child_pid, "time": time.time()}) except NoSuchProcess: # already dead ! pass @gen.coroutine @util.debuglog def kill_process(self, process): """Kill process (stop_signal, graceful_timeout then SIGKILL) """ if process.stopping: raise gen.Return(False) try: logger.debug("%s: kill process %s", self.name, process.pid) if self.stop_children: self.send_signal_process(process, self.stop_signal) else: self.send_signal(process.pid, self.stop_signal) self.notify_event("kill", {"process_pid": process.pid, "time": time.time()}) except NoSuchProcess: raise gen.Return(False) process.stopping = True waited = 0 while waited < self.graceful_timeout: if not process.is_alive(): break yield tornado_sleep(0.1) waited += 0.1 if waited >= self.graceful_timeout: # On Windows we can't send a SIGKILL signal, but the # process.stop function will terminate the process # later anyway if hasattr(signal, 'SIGKILL'): # We are not smart anymore self.send_signal_process(process, signal.SIGKILL) if self.stream_redirector: self.stream_redirector.remove_redirections(process) process.stopping = False process.stop() raise gen.Return(True) @gen.coroutine @util.debuglog def kill_processes(self): """Kill all processes (stop_signal, graceful_timeout then SIGKILL) """ active_processes = self.get_active_processes() try: yield [self.kill_process(process) for process in active_processes] except OSError as e: if e.errno != errno.ESRCH: raise @util.debuglog def send_signal(self, pid, signum): is_sigkill = hasattr(signal, 'SIGKILL') and signum == signal.SIGKILL if pid in self.processes: process = self.processes[pid] hook_result = self.call_hook("before_signal", pid=pid, signum=signum) if not is_sigkill and not hook_result: logger.debug("before_signal hook didn't return True " "=> signal %i is not sent to %i" % (signum, pid)) else: process.send_signal(signum) self.call_hook("after_signal", pid=pid, signum=signum) else: logger.debug('process %s does not exist' % pid) @util.debuglog def send_signal_child(self, pid, child_id, signum): """Send signal to a child. """ process = self.processes[pid] try: process.send_signal_child(int(child_id), signum) except OSError as e: if e.errno != errno.ESRCH: raise @util.debuglog def send_signal_children(self, pid, signum, recursive=False): """Send signal to all children. """ process = self.processes[int(pid)] process.send_signal_children(signum, recursive) @util.debuglog def status(self): return self._status @util.debuglog def process_info(self, pid, extended=False): process = self.processes[int(pid)] result = process.info() if extended and 'extended_stats' in self.hooks: self.hooks['extended_stats'](self, self.arbiter, 'extended_stats', pid=pid, stats=result) return result @util.debuglog def info(self, extended=False): result = dict([(proc.pid, proc.info()) for proc in self.processes.values()]) if extended and 'extended_stats' in self.hooks: for pid, stats in result.items(): self.hooks['extended_stats'](self, self.arbiter, 'extended_stats', pid=pid, stats=stats) return result @util.synchronized("watcher_stop") @gen.coroutine def stop(self): # stop streams too since we are stopping the watcher completely yield self._stop(True) @util.debuglog @gen.coroutine def _stop(self, close_output_streams=False, for_shutdown=False): if self.is_stopped(): return self._status = "stopping" skip = for_shutdown and self.use_papa if not skip: logger.debug('stopping the %s watcher' % self.name) logger.debug('gracefully stopping processes [%s] for %ss' % ( self.name, self.graceful_timeout)) # We ignore the hook result self.call_hook('before_stop') yield self.kill_processes() self.reap_processes() # stop redirectors if self.stream_redirector: self.stream_redirector.stop() self.stream_redirector = None if close_output_streams: if self.stdout_stream and hasattr(self.stdout_stream, 'close'): self.stdout_stream.close() if self.stderr_stream and hasattr(self.stderr_stream, 'close'): self.stderr_stream.close() # notify about the stop if skip: logger.info('%s left running in papa', self.name) else: if self.evpub_socket is not None: self.notify_event("stop", {"time": time.time()}) self._status = "stopped" # We ignore the hook result self.call_hook('after_stop') logger.info('%s stopped', self.name) def get_active_processes(self): """return a list of pids of active processes (not already stopped)""" return [p for p in self.processes.values() if p.status not in (DEAD_OR_ZOMBIE, UNEXISTING)] def get_active_pids(self): """return a list of pids of active processes (not already stopped)""" return [p.pid for p in self.processes.values() if p.status not in (DEAD_OR_ZOMBIE, UNEXISTING)] @property def pids(self): """Returns a list of PIDs""" return [process.pid for process in self.processes] @property def _nextwid(self): used_wids = set([p.wid for p in self.processes.values()]) all_wids = set(range(1, self.numprocesses * 2 + 1)) available_wids = sorted(all_wids - used_wids) try: return available_wids[0] except IndexError: raise RuntimeError("Process count > numproceses*2") def call_hook(self, hook_name, **kwargs): """Call a hook function""" hook_kwargs = {'watcher': self, 'arbiter': self.arbiter, 'hook_name': hook_name} hook_kwargs.update(kwargs) if hook_name in self.hooks: try: result = self.hooks[hook_name](**hook_kwargs) self.notify_event("hook_success", {"name": hook_name, "time": time.time()}) except Exception as error: logger.exception('Hook %r failed' % hook_name) result = hook_name in self.ignore_hook_failure self.notify_event("hook_failure", {"name": hook_name, "time": time.time(), "error": str(error)}) return result else: return True @util.synchronized("watcher_start") @gen.coroutine def start(self): before_pids = set() if self.is_stopped() else set(self.processes) yield self._start() after_pids = set(self.processes) raise gen.Return({'started': sorted(after_pids - before_pids), 'kept': sorted(after_pids & before_pids)}) @gen.coroutine @util.debuglog def _start(self): """Start. """ if self.pending_socket_event: return if not self.is_stopped(): if len(self.processes) < self.numprocesses: self.reap_processes() yield self.spawn_processes() return found_wids = len(self._found_wids) if not self._found_wids and not self.call_hook('before_start'): logger.debug('Aborting startup') return self._status = "starting" if self.stdout_stream and hasattr(self.stdout_stream, 'open'): self.stdout_stream.open() if self.stderr_stream and hasattr(self.stderr_stream, 'open'): self.stderr_stream.open() self._create_redirectors() self.reap_processes() yield self.spawn_processes() # If not self.processes, the before_spawn or after_spawn hooks have # probably prevented startup so give up if not self.processes or not self.call_hook('after_start'): logger.debug('Aborting startup') # stop streams too since we are bailing on this watcher completely yield self._stop(True) return self._status = "active" if found_wids: logger.info('%s already running' % self.name) else: logger.info('%s started' % self.name) self.notify_event("start", {"time": time.time()}) @util.synchronized("watcher_restart") @gen.coroutine def restart(self): before_pids = set() if self.is_stopped() else set(self.processes) yield self._restart() after_pids = set(self.processes) raise gen.Return({'stopped': sorted(before_pids - after_pids), 'started': sorted(after_pids - before_pids), 'kept': sorted(after_pids & before_pids)}) @gen.coroutine @util.debuglog def _restart(self): yield self._stop() yield self._start() @util.synchronized("watcher_reload") @gen.coroutine def reload(self, graceful=True, sequential=False): before_pids = set() if self.is_stopped() else set(self.processes) yield self._reload(graceful=graceful, sequential=sequential) after_pids = set(self.processes) raise gen.Return({'stopped': sorted(before_pids - after_pids), 'started': sorted(after_pids - before_pids), 'kept': sorted(after_pids & before_pids)}) @gen.coroutine @util.debuglog def _reload(self, graceful=True, sequential=False): """ reload """ if not graceful and sequential: logger.warn("with graceful=False, sequential=True is ignored") if self.prereload_fn is not None: self.prereload_fn(self) if not graceful: yield self._restart() return if self.is_stopped(): yield self._start() elif self.send_hup: for process in self.processes.values(): logger.info("SENDING HUP to %s" % process.pid) process.send_signal(signal.SIGHUP) else: if sequential: active_processes = self.get_active_processes() for process in active_processes: yield self.kill_process(process) self.reap_process(process.pid) self.spawn_process() yield tornado_sleep(self.warmup_delay) else: for i in range(self.numprocesses): self.spawn_process() yield self.manage_processes() self.notify_event("reload", {"time": time.time()}) logger.info('%s reloaded', self.name) @gen.coroutine def set_numprocesses(self, np): if np < 0: np = 0 if self.singleton and np > 1: raise ValueError('Singleton watcher has a single process') self.numprocesses = np yield self.manage_processes() raise gen.Return(self.numprocesses) @util.synchronized("watcher_incr") @gen.coroutine @util.debuglog def incr(self, nb=1): res = yield self.set_numprocesses(self.numprocesses + nb) raise gen.Return(res) @util.synchronized("watcher_decr") @gen.coroutine @util.debuglog def decr(self, nb=1): res = yield self.set_numprocesses(self.numprocesses - nb) raise gen.Return(res) @util.synchronized("watcher_set_opt") def set_opt(self, key, val): """Set a watcher option. This function set the watcher options. unknown keys are ignored. This function return an action number: - 0: trigger the process management - 1: trigger a graceful reload of the processes; """ action = 0 if key in self._options: self._options[key] = val action = -1 # XXX for now does not trigger a reload elif key == "numprocesses": val = int(val) if val < 0: val = 0 if self.singleton and val > 1: raise ValueError('Singleton watcher has a single process') self.numprocesses = val elif key == "warmup_delay": self.warmup_delay = float(val) elif key == "working_dir": self.working_dir = val action = 1 elif key == "uid": self.uid = util.to_uid(val) action = 1 elif key == "gid": self.gid = util.to_gid(val) action = 1 elif key == "send_hup": self.send_hup = val elif key == "stop_signal": self.stop_signal = util.to_signum(val) elif key == "stop_children": self.stop_children = util.to_bool(val) elif key == "shell": self.shell = val action = 1 elif key == "env": if PY2 and IS_WINDOWS: # Windows on Python 2 does not accept Unicode values # in env dictionary self.env = dict((b(k), b(v)) for k, v in val.iteritems()) else: self.env = val action = 1 elif key == "cmd": self.cmd = val action = 1 elif key == "args": self.args = val action = 1 elif key == "graceful_timeout": self.graceful_timeout = float(val) action = -1 elif key == "max_age": self.max_age = int(val) action = 1 elif key == "max_age_variance": self.max_age_variance = int(val) action = 1 elif (key.startswith('stdout_stream') or key.startswith('stderr_stream')): action = self._reload_stream(key, val) elif key.startswith('hooks'): val = val.split(',') if len(val) == 2: ignore_error = util.to_bool(val[1]) else: ignore_error = False hook = val[0] self._reload_hook(key, hook, ignore_error) action = 0 # send update event self.notify_event("updated", {"time": time.time()}) return action @util.synchronized("watcher_do_action") @gen.coroutine def do_action(self, num): # trigger needed action if num == 0: yield self.manage_processes() elif not self.is_stopped(): # graceful restart yield self._reload() @util.debuglog def options(self, *args): options = [] for name in sorted(self.optnames): if name in self._options: options.append((name, self._options[name])) else: options.append((name, getattr(self, name))) return options def is_stopping(self): return self._status == 'stopping' def is_stopped(self): return self._status == 'stopped' def is_active(self): return self._status == 'active' circus-0.12.1/doc-requirements.txt000066400000000000000000000000251256046442300171150ustar00rootroot00000000000000mozilla-sphinx-theme circus-0.12.1/docs/000077500000000000000000000000001256046442300140215ustar00rootroot00000000000000circus-0.12.1/docs/Makefile000066400000000000000000000131451256046442300154650ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = ifndef SPHINXBUILD SPHINXBUILD = sphinx-build endif PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* $(BUILDDIR)/source/commands* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Circus.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Circus.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Circus" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Circus" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." # Need it to avoid error (hack in ../Makefile to manage dependencies) bin/coverage: @echo "Doc updated" circus-0.12.1/docs/circus_ext.py000066400000000000000000000022601256046442300165430ustar00rootroot00000000000000import os from circus.commands import get_commands def generate_commands(app): path = os.path.join(app.srcdir, "for-ops", "commands") ext = app.config['source_suffix'] if not os.path.exists(path): os.makedirs(path) tocname = os.path.join(app.srcdir, "for-ops", "commands%s" % ext) commands = get_commands() items = commands.items() items = sorted(items) with open(tocname, "w") as toc: toc.write(".. include:: commands-intro%s\n\n" % ext) toc.write("circus-ctl commands\n") toc.write("-------------------\n\n") commands = get_commands() for name, cmd in items: toc.write("- **%s**: :doc:`commands/%s`\n" % (name, name)) # write the command file refline = ".. _%s:" % name fname = os.path.join(path, "%s%s" % (name, ext)) with open(fname, "w") as f: f.write("\n".join([refline, "\n", cmd.desc, ""])) toc.write("\n") toc.write(".. toctree::\n") toc.write(" :hidden:\n") toc.write(" :glob:\n\n") toc.write(" commands/*\n") def setup(app): app.connect('builder-inited', generate_commands) circus-0.12.1/docs/make.bat000066400000000000000000000117611256046442300154340ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source set I18NSPHINXOPTS=%SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Circus.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Circus.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end circus-0.12.1/docs/source/000077500000000000000000000000001256046442300153215ustar00rootroot00000000000000circus-0.12.1/docs/source/_static/000077500000000000000000000000001256046442300167475ustar00rootroot00000000000000circus-0.12.1/docs/source/_static/.empty000066400000000000000000000000001256046442300200740ustar00rootroot00000000000000circus-0.12.1/docs/source/_templates/000077500000000000000000000000001256046442300174565ustar00rootroot00000000000000circus-0.12.1/docs/source/_templates/indexsidebar.html000066400000000000000000000002301256046442300230000ustar00rootroot00000000000000

Feedback

circus-0.12.1/docs/source/_themes/000077500000000000000000000000001256046442300167455ustar00rootroot00000000000000circus-0.12.1/docs/source/_themes/bootstrap.zip000066400000000000000000000372421256046442300215160ustar00rootroot00000000000000PKz?globaltoc.htmlUT NNux ] 0 >EEwٽ%-t w_uc؈eN_y>cT wO,Byrl/mBuJ{yLզvGkIQtݒꗷ$~~0b0PKz?>Lf9 layout.htmlUT NNux Wmo6_qPZNm60 ؆C? E@Kt&rb{"Ynݾ$xpNrYJE.V6Z$t;y8'-B׍YՂn_ߣcRX͚iUS;cF:JlǗ˜^OHlZ{ˁ쓇SX|}wJZZEN)/$Bak%3: 8؉" 'V5P,4OeB \9viZKuȇ C*謣.5eBy@ UM1vS)mmmq6@^+:0ov/* .Բm)jq+> >bP=9 3)1Z~}Nמ'V<.,N*3]TϢKurd nv_ӈEҝB m8a֥V|#$N,{? ϵnlũ%b[[2 ؆E!V KJiC94L5KW]yDIl*q準|.[$%>( xQzyE!M&|]'dڜN&4t (_$Yӑ'Tɤ+5m2ć]ղ|~Ŗ(AUZ2DefBx _XCbrדl<3߾ѫIsgkW̉M|G bYh]=8 \.$ _omגFpd'C=gE5X\<$.Jh<ZJ']3QhU]_PK z?static/UT NNux PKef? static/bootstrap-dropdown.jsUT  VNNux Tn8+QɁB%AVXWZN聖2SԒ]ͿP'-Cty潙x'PyukUj+ٝ90[a-jV,S 7e+._vL..UӢXY8;?^Gd"2se !xF6'F(  |跡ة|RY 0%~˰ $\f$biYm9rsJky n[m_-Xt{ '8Iǧ\µ,_-4]W$( Y-( H1V"v5:\Ģ#ЏR>8 M<0M4E<04x;['NF uJ;$SNb޴-E|$aLXɢB6%9 Z7QCsGS6G?bD8 {ދe-3W‡ z}-aFhz}h.NP:* tU%f:4Z6ʐw"]|r.E@xW/8#hRx `-[qsYrc|OU( zf%remW\UEQc^`{x6vSHQ2f'di(O㚑?50ӽ`ϡ]BmQ{͟GwPK g?J static/bootstrap-scrollspy.jsUT CWNNux V]S6}ϯd3k>A,[O%; 3-;2!Vl+}(y[:sՇ}7}XJ%iqPFRdYYlC X*UZsS*hCH1_Uox&R%y㼆\%c&A-L ?3L\pрb#*X BAU2%$Oe;-ڌsQhYVq ?aO],N̫(-?fyg>e״mboUu{ <#{휶^o.^h=wEX)%rS]n;~?eTcPO۩(W1&cfYF˒8-A˜;Czpxogɸ=NxWFO-|NS8o? ۦJ(lW$&Ud+0؀b[P!zMgg̼OE9ϢoU\\ttl&dC>cV_u.ҽSwR)c/vP;Ws t3(j ɲ6&UDt)Cdb<25FxMeV-JY]z 0^A8i 베#4G5x[9U"PKXT?c^%static/bootstrap.cssUT 6NNux =k㶑o~ϩw$YV'wqUJU9+FHvvƓx4@Jًng!<Fh4Mj&lL!&QFt6}6GїeB'Ydit.ӬCD%r1tgJL᳂j>_o8UȚФL9/?D(X;E>XEEǴb5'#mv}s.Л(/xoYgw5Y;)RW&juD_eO#&Iu|::M7D5"D}Zgѹ%")9СYU:@(9MTۨYTY8?}՟ȎY e9hJ*dQQԐiem{GQҧ觻(:!/Dh>|w1|{K^N]Q%~8WmkAnWϤʧ#@ftTR3 DˠH~|0Bw~u.c4vt@3i $讂ϳ"a(Ȏ6YdY Uv維 O>Sz=LZunk'|aJE@fv9~)Tr>4l|gОf(0hb`qIU0Wݸ) .,:r7feFe@AdO%6*OKbu} "7:>vvfx֘snZı|GUrnxq2("$iYmP6!MΙ%yP&{P))(P*QarlIf -HT#k(JH4=4VE1EocUeվк R4[- z2#igA-Xi2mCo}4NQ,x" ֢a:9V}hU9>L ߴyy:&0 3bQ1=ۙG锑  򓃀UĶwW}# M|{pM2NrՍ+9J0h-Ј9ލҊT Ռ`aJ ŬzT}TQP̾?e7e&a :E{PAOyePT1(|I~_F r8a-?Վ[-AaPi<Gmm}Ns A1'͎ ۬yavIi\;CΫ_?G'ʊ}2T(0OA5ղ6󽽴OrlxYbJ(Nt">yb+pEo9 k[jkUTԩjbZ綢ჩ%{2}qι/nGY!mEoc+UtW&fNvGф*:;R[ *-k-;͞ 3]`iӒ| Q stڦ+*U:,%ŹJ5]Sg5o ۅK=fr:ZI,˧=ҳg{:ؤ9TpR,5ƊP&J2\Cy}i2z"XԠ=o bK9iYs#kg-hjidm57XY#@cg-O<A dfPdm23H1q1h1q15qY[cdnL2N<53 |b6d[B/s l**+6h-8[%8%HZr閜Z!%ݒ G)H3F >͐aZ 5=nyqX21mq,&q ]}ۧSÓ9h$0n,>lrL>¢hŒO3RP.Ub3U5* 0l9N3: Lw_~ChW_b&՜H6kZꯕ% ZB?33_qJHRlD~lċn!]p^=-,k"酹=,ByP3菰05 ;0mEKع̔Qv~ l dgM,F^7+Kr6e~+rwfJx3sɀ%X[' ԇ(m<bjT&SoOVЄ\! %a[_e7dx}`vt&KFŷxnRLJXo^;ۙ`L +h5r\Z t!Sfcy)hN7m"і!=WhFs*I+WUIjL ,>|Qk_g#XˊdjuV5>"MW,,kʲmhvtzp%M0K9J|j3YDwZI񠉂wͱ6})A[& ug2<$x.dmiPT{;܅"mBp>O>!2 |L<:Y2- jlk]v~e[2WLWY2lorU(@n 1HvFI| /p Y ed(LX `lhLp\xk Ѕhta.]D}:XڝM*^%qB VC5 m44_Ӑ2BZZX--iIB--Wjiu? apjisMK!Z^rG̦ĝ^9B*)3[N̮!|1E*Y3[X̮AaUb37l1Jbl{x;Gmr!S$zF/TeURT5miSi,®6Xn:,GkjY4I-V9;Hzk!pnc j4YR)Ƥ8Kmۈ~ˠN{'@gep7}_ĎeC |+}f:'K4GZպy tWN/;#)jp Y [5#i SJê:뼟PL*Ftu*@ȯPkyH_$^"iHbI?o0^C%Bk}<)ܨrA+7n-%NR[.k1 F4$PsI3T @4&,< KQ3Ԡ hwGlW1|x=Cj8o5- ʭD B$|mí) ;)-K\d bqN)`h 4N[^-Nd7U$--c!m-fhGQ̺Ԓ4#֝mf1#|hɆ0H6.s2zh'i/PӚ/ҭ)`~]L)q8@vZs \ 5Ywd>Nk.~ʭ_ɭmkv/. 9FCH!fh_WGۘqu1zL@LʠrҸî}!6<+Sԫ-p!Ue7yԭc~td_F* ,&!z.q[2C_֒s1 @;p/(t^Ĉ7Ź=ЃL#RvБ?4Na[I׏|rqH]|EFL?.e%:&׻/p'`fcfNwu&u)&kV^PFFJ3Y3"|VnwC}CI[ƆNG| Kzue1tY؍笁3_S _,u@Edvpsq'cbʬrGW {ΏCTz_*T/b[~Oiȁv0H<=d47jkk#u4,gU^W?K [nUUSۉִnDi7 Ld$-Jz#o\£t{imu3߃OCMJ>`+S~uIz%/ٮ 6"@a)1UvڡtC}GA6cWX̰DA f4D^Fhr8p{9CZw@%Ǵ(ºݵy;>f]')+yGD;b[=<ў7 E4fճ\OPg4`|z8#τыH./Ó׃ozKx)T %&EeAd5 \H:9Zg] eCc4lޭհU CєW+'^f[vE~]o]2_+X"WwWh ]Ѱ`W:"+\K}#9>iCd_L #O}A&ANkLSzNh@QܰtBDFN7tڭ LmeNurY-;S%cyn!RzI1J%joHYFhH:*ܨX ȸ5VjD3)\"!P8fonr$fmltf2x$}Pt+CκTSux/}pLwBip/{3u! m}e.q$K=&zO?{Ưн9Db{`gު=y=7н9XG捄uo{@DOÁB? cM-˥H;9]ʰȗ5lFS7i"^x']yS^uCX&)_h㐻,o"dޕ/{j"'-,x_Z*Dw$["+k浸>= aے9,nݷo4Sǎîek`s-*ʵJ4/-UX[1ibbơ$oMݺ&|cMU,V+&%2_<)T}u'eOσr I973)y#n{s$ '%[I#\.w4Ǿ+#2_<tT}ucOσq 873y#n;WKLG<V;Y ZV xP83ZF=YaSs)jF'/g/ʂ0x9ofEwhuy̲no)x>u|GvKlY3b}zu_]$THF>LBL[+zҹE2 F_<`'wvvaO2"Ny<+3e$ѿX;Xڙꫫ< Dާ > Y6x#8\|A؟YR'tk!A_>K?!JxPc[ Ɨv .+y[h2yK_Kʾ!>{ENN%UGou sq^`op=t2g̲12hkOZ2D :^g3,Q%C<)d!i;xI[a#z_y*Kœ7!\H#ݲ'LI*ʼcwj9 ? }_ݗwоܘvm" FkR|<7'<Ϙ ށyU;D\gށ˸&-EF9{=c]BA_hgܺ ;ipG>`cwq94LpZʦw:'UJ ^/ zg4JdORޯ| ԃeԄ6 kc6$Œp+]g<3}1d)b#ß9].)lD7 ͥ.1Hٷ͹ك,+\F0bՋ3P7)0d.>gչUX_W^uUPE4)DvwIH,KR1{#'cC WsН/ݒ .f_/M~d管2Hd^mÎ3b cau:HhSy[ibjH/4EG}0c}hAK1GZEcdG_@]1~=0sb4hs0a8.֢mj4FKB&}l* 2ҢC]DϐF|q;]tQԩD!o"kI/p=T3]v-_nDpaVKw\ꢛ#GFrՍ!Z"x$b 2x_"-/EHew9&ϟNVa:1OI *%;muyٜlmvo=g߁pVA?Cp'*Nh[Q]IdNj8dʠJW]V7U8pӄ]lHNGj ke*;;do|.[2't4\R=ya'59yT͛pO㩪[h2dH坘tQDv"˅^Lf9 layout.htmlUTNux PKz?/n localtoc.htmlUTNux PKWa?)"a0zrelations.htmlUTLNux PKV]?d-bUuT >5YcDDc]ncL6Hk " Dsod;T _c ׬53ƘF㋫W>rʿEk7m ;.k~R QrCǣ mj\bRD;9n{z/=/2k׮[..\M6i}@[Νko]v7ȑ#޽?ӧNJ5>>>l64ͷw+"^]-bFueTeM1""B ("n5F" 4j01VA#R8ib\K1YT2dF$BTT#SUŤ0e vO8z׻ʯRׯAV{m=s9$J,XXh@ ߀]f""zh,ϟqj۷oADohfYIQappPvg͛fw}7ǎc۶m3>>n]F6Zz!C^=~-N?4~l8qwsss7ns裏VԺx␪Ѣ(GBAƚ&uX 1!DC B):/BX$( ]TC!Cb2UqE8XT $Ed#Hc5\UH>jEqnWe."ݽ{K"z)bŊp}wzbZ9oU&LJADS1A F5ՠ(#*5""bEU}|2ѫ^! 1dh&SbFCT64VB$"C?S_d{nye {/c`T4m#MDZѧVٰJs#ҾC^A4_!ĵ@R/! "8=Xg>54.HccLػwgUռ⋏yk;˖y_q<Nk+wm(|{/Ƹ!˲!M1FU.cmyV-qj]jN"@է[ KޣDLVi"@RZH\@4UcTTU"IB5V)ʈQR&$/hH( hT $BA]BX$F?gfoo~%DV KEg=sēHC%"b2X{JE`\EB*AR$_4WfcJ6& #nDl 9b28ؚTX&V,K'N|G#b Y呙9df81M5:y=3g)PkU sy޸Juu%#Jq z׈PC:ZA(TX4 Ħ;#,=2bkDGLV3K8*LSM;E ՠbl"@# VLח }t4&c1`=c#d[ud/3UB_v1?]~իW>h,=hld`uQAR .9HJr>=E-Y9Fd9A7X09b\]fqYA$WOc٤-TN=%k1>)e:bU+$te ],*V Qֈs e{I]\@kPN a y[" &t5[Y؛@jb;X bjmquI'b,1K4: 40&#Q_3k4^JM2Q#Ĺ+0+SYfͪo:k֬7^$|ۊ&:s/乻\#g}}2օΉ341"bQcD6QT4JTըV{ R'nD@1m^Z9 6kF/ nԍ {DnL0fS+?Y̟8sң\+Dl#Wȳ/KvܹY 4v/.&һv/AӨOy4P %^VӞBW1 QPQ%֎( f*1k*Ul^qChq͛QvɈn\4uƉMj#& &Ad}4m.n~zǎ>ϰa}t}41S㿈.QɆ$aД8ŨM IYI1}:Q$j2=/\W滑Ce}#! "NLF !%|dVW'dS_1yѵtd{iJCd%j#'xnKoU{f!v!g0V .zBϭ;oUHM5S<(aj)mUТ޼ NپsOrK?PbEZq5J4T)MHHNjJ47뉉z&_Mb־+h{ 'yΝ>^xVZŎ;R~5h7ǴĬ|LcrbUDRu]Jq5n1*&AT}BĠRVîN6m5UbBT]T(}eȆQU\`Uf~wW &cxNvϦ~/=%OVC6>>"USTz*!%}vJĨjH鐆H`Zk|$6O^>ELڻҖd}6%#ʦډTFQQIT1H¡(k_FLݫjm[:t?s }gX*2JyKGH9s(5nLJ TʩUus]KA:HWQTU QQWUc.?;DjϞ6>'Wn]['?7^jB{3'8,[^:zgEOFJ ^ڑ7ʦ(g(bW5q)߯O) D#5]0Wʅ$>yp`%(q*DF*UCkh,JLVףx@~ϴfvY:.kQ*H.Sj/OE&6N2q8zH9ZE\}4C@2T>/ZCc ~Q$VY.Ipcbl۹s'WE|D{|cb"к,LdYsZZʞGS'25%YK)W5BDnpUifµ^L+B Q!*TeI[49Bc3O2gpGZ7(O}ٙWh5R؜UMG683g~:=XbS Qcڣj5$߉ 6e⑬&EIQŘ'(,dS% =F.(Dl^oA G_y.Q &,V5ٲ`|.2>㹱(.x z cʪQPvEԗg(1|t`\@]\LP;qڅ&z2PM*x|+:u7/ޠ0r`7H n ֧ࡩ1ۨ.\%0E>×qj ?7D*pc^繻貊&>Uq"[w4i #3[ymsghFjTˆ,d*%*w;2 aٝ4vPPB ԁfЯZV?#iR`ӢeD0͍_W^ѵkMkf˖-(9wʕ+p·A&''r ;we.{ѣc]ou"qEe`:jj]}:ړ $OP aŚ{%ZyA+ܵ65ʴӋRT92jHfaAcY=_|  n@P!ƪV5:l}z<,Yl<|BO^nȹ=nǺa%b+0|5Vǘ{2t˱ W8٦:y/J{LK1lk{Hx7 P~6%Eh HYmf)Nը^9mTGfs'.ҭ"m4r4v5th<1OxlyPI?9G41HJQjl l,k5mu+z?~f˖-ի]/$\zkZ Tը1fߔ\x/sǎcxx7c}gl>|5ߣ*+kޥqᴡwqT֤bM9Hs #Id1",t,_(i("jU\;ZzVxym{,ַHп5yhX/,QpoMgnH[;82{X ~]]T xaq}fcxm IǙNI22& W{Zl) h-e*^Zp1Yu?,<;?Ν;zfذaEQ<Bxk Y4kEQ|ipp$2|pp(fB^ݙ>Ǫʿ{];p@>oر+n2F]<.NrXG1:JR3gA-H*@ (QcA1oQ,s &ddH2 "U֕t%IRa|U]}FVgj;2). \_owrOYLwdX_\Գ̟?hJi IDATak=Vta5r_eGFD- φahkʕssXf} Dh*A3W5$5@K@^&KE"ҔCUd@&79=5bʱWKsʱʪ_ԹMQz5ʁcn n̢ƪ:|#3gdYecFwQUsb!oUlmLu{NJTF(wi1ڱ{g;]HUs7nٴg?w{(۷gMsmXšC'542HI؈ztIHgd0'XE\Fc<&YʄNYӚ#.eP18Ƚs^= |qb˰fPebWoIA.}O a\2 <͂q*L +3d9vpk $zK6\1-9G+c@T5*oK,`0TɎ u)ŪiH9C1x6iΨ,=[ `C0JN UEzhD'JR7WXqDmWsv+㡡3t%"9vE!\v.Rۮ%*:ymbcl9bo!ŸSӳů...>cRk`ƪ+lKTRV&m!U2p6x(7"1%S̝CCª b(f6h IjxD=l{+Gl~XttEAg-\ ^W6ZpA[ (@)?țF)L/ldque9ƢbSIYavAujF`\[mI24(US[fUIb=/.w;^+._6Ymm@SO!M;}]MX vpv}=8k ?$kFb&K@U+SkkG/5C:D]4M,^A,0,rXWm AqM>NRÙ.dK;_p3ɩ??Ƅ?ŢSĒI<E~&j Bg}'v`'M6ta\wpTQn Eܰ432YIuK8`pTBT'|j=X Rޓ߇aH6`BcQOH[DM}̒ʩ7uq %'/,Z2sANJZ8,HjČQ^/>d~A<ȁ{XO"ڣ:/uAQ.)T㕰}^(MdMhҟ} r咐P<9HN͙gȱXxKӒ"t@–m`YA 9аLq9נs\-5h7\:MST">Tu 4h7śB R f$K!%bTTA[fyqUp`PpJ+BqA&ߦDE٠!Uc$hqoǨhKxpN!j>(KM I1EO#^5% %NhjLhsQp(J ~1j,<͵mԞr d.:N_P ]7cSSMWPƠ1YP*bŠTWm6D ֌ɖG20*aH1P=XtC y`jjꛓU} !X"@Ve$Ʋ%8i4Rt FZ{]2ə8E:!QbLJ[-=ѐZ 37]"+=W>v\>{% 18v5R.Sodԗ*:Ta5CS"hx-K( H[.^,+ǀcR™Ԃal22hh7 s VT {S="̟=3^o{w:zѣG吵[]KÅ:7(bP &!bTF.~gGUo{g$u{}!$EJ")+в,vʲ#ˎTJ8lT0DIbM ['ƈK,H(Tu߻s ,[O{n>XΞ, JJMl D.!GmLI(b6kؖv#e[r&H | #Z-ʗ%` WaM{+6v7GCq8:{ رNbg%FAJkG +|QKm_k,h*bF[vPL 4H0V@16X+bL * )ȹ1&"E|bN$P^6jjpT0IVvB?{, rG~eϞ> Hɉ lzW /_W^vcǎu)f!ro/v-TCo3erOnN$e$SH6&sDLj\4;KR ѬQMQ_u7X PcnvZ]|7 ~Wv\6I < @ vM- AaZ"H}u5pvЮ V1be?3],Ӎ-BP^_*gυطdRےXL^UBY "~CEl%sX" o:*KJpEFXVvt L6 ^x,4FP22f 4ոt#E^R"H'o^gv@q]:a*m#ykR[n? rfjjƿk iEΞ=K۷o牉vȑTץ{ t`Wm$=ݑJf^Ôe@H&;+/c58 Y)Ȓuk:%EDM 1KR&O"w7b$Q5TxBEĶĐrdm-$bU= kLV Llc?^G10*UBv+^pJm1 /cӇ ^%O4bNs++VXCXޟϿPZ'$5a,%>qgmOY>NX?FsK F&VqBp(tngCB 5&m4&hpKᡀ #:ӳ/X@obvGr"GʭSnҨlM\|w6mwb _׳Y #ۢlݺǎnɓdt)kR{A/; SI2&:idJ\GStQЖ)N ԐהCd~Ip'1p`*l#ȠA"LdjÅlMz }f6&'o?2h&cEJ( mk*0KA,v =07͸_x'|\B.p?b?xnĒ,h,0t|nC?%*ĵz @i#Enxa5EŜޒQ׫6 t8#W"OEiDa%mG׃ ,K%j֗d5&!\Xt$y_9NC"]h8EԖʫ+|g6σb˖-_cZƽ{d$Ase!s~#a%敓fA6d6i'N\ \&ీQB,ɒqʤsm9p۷_l@foV-֕$Xg-)tidˁjB[h1|Z?^SX9>1",UT嫨"`İ6<[ad2.\0/Q/3"-r]wojSr5JDaam1&(恾<710"cv4֎clf9MÌA)ReWg|@h aYc`tPA@oa$Qh)>5 .z:E V|;NކBJ b] .gkuVvr¾LW@z^p{.|d6.CJ d5Cw8?3'h 5$` & A(޷ ,НDFF>$zKJX ߄ag_ ~ee$;9]M +o #cԮKH 0)OP8 #Y+* $zہ˸Ӫi)>X1b&B۶mq;MZqw4_cǎ 'O456q·~X=Jye wNZ3'N ZzV"Vp$ow8 m)*}ĴnI@g&Mrq ߚ@2,Ay\ .˸C&R4TEz:%:< @GB(zΫ،y+WQIDAT6xp hVІ[ -NDA9(In/HӔ]։DْV$ !`D0t*;b5OHo~e8H&_5ӧyΝ𞜜 k#ϋNL}$mX O]x(O9o>W؃(NQY"-/ńm>IZ.G,Zu!]WEi".o)֍Hx HSj d6f[=] ke\u?]Y'1iE\F=\/>Ãy YiF]aX( 6ˬlA,8˼QV_20TfYwd.?m;i5FNkN>U@l@l`Mj `Fb$ ; i[YfQ4U A@)ȅ .zE GjcTϑ}㿳GE#֑#?"4Myvl߾333׮]:p޹sW/_|\.$*@S$;\/G7aui 4bZ$H %@E6Pwd n J18~"ofqgc@ n5 Md F8fO |oNqkGcP@KRRFzaĚMrsnR J?m /pͪ P%yE ]*(t "ld Hff 2&1ԁ8#R Ew*tZ$YxaY@E5Fіr' D Qz5wH`u2^E0ꙤG}xlƾ¹sh``;;;oz*MLLb$?cTR +'P_[e:{-ƾ6أ!`V01ȑ$7i\>KO"S/vWW\:$4P{ z^r(c&nkwNQξNKG~ ?.OF`I !Tp}2_W e]Hh~1tI@\g@̅PWn?󟝢#'_\ w[g, I QW~-s{ "ڳEvȍBLA7,1ː @$Z,qr Xk$a!AB*V4MV RPJ 01H) A|%?,!Z:?lCWQb0(ysG '1bӠc;6/K}\eMD"󭦬)("hz b *0njhDg2?~FGG{@>ׯ_277Rds+%K֦h,^W-mи.kJToZT+!&7BF˒ apb<|R nEš~Nm4{/iܙ/ N uv:ˀf% UQlJ&RWJ[A/(`䆈0#v'4հ&$geYЩ^_%Ir^k}+KfF$SqOJ%y(P(9^juxiii_Rj55 ?a-$I}adaثsK ,KxJPgtz;$:ۀbx _=L]c[(G 3<zyLS{!aؚW޷oߗsvfVaZ~T)xR^Qz( s- 2Q s:t"CMjZ)0@NJشW!,Zm0ovلn~muǑҙ*u{w8I5YRk{$LԌ08Cb"f5+o1<"&E`9nBD|D}73~%䓞B*W/~],SfHMkJ]EhB_>\a㐇"n@wLڲ&V+Vdneͣ`2jtpsKAF@<22{v4}ܹG} ?_d_BvˠhQ|_o8g*J6AKy=Kix25Q1Sb_ 6JԀ՛sXE/@kLO<ܤէ-]FpG DnYuT8 @X( %iUJ-cNX"+Ƙccc^[nahhmX󘞞ƻ}ԧ4 lu,S\(:h+FĨ@q`MyF-8hfi#+epq]c`n.4=4ؿddyۉ6\x.];vÇć`8#+--&@f6f $]7 IR4w]rE,>ܑ6\̵e7k#x xe,.!֚!<3F :B8:d4$4DHA3_).h'ise߾}7ى3sFT;-_ʕ΅TJX4K!78-d DU͵H4bp0j1ShOP}O٦kԛIe-k0k&h2k&`WhS͛7[c`fnUXJwBf˙~[CB&e!k=ȑBhx84zPd< 7+X""XњUe{<{Tj> D@11B8uTYdthb]kUBqXkVیK_gZkAGGש\.Q4fOHKRjM\܈ 1)D$  >'r3Xc`$@(bese ؘD@0 `Cd6~_5˷8 ^|EaĹ ^?][w Eg/F}=zڻAlݲ 9BX"ic,3_Bkkk;qhppp}Dc?jH(?d6 dJ+W4 8!,IqfIbdg}R3(%fM& 嚨VsD@{Ma\2$ċO=i~'Nо};9ydK:qƔcn y~+\N^9(Zl9uRPJAy>(@$z!> Jq"M55)Zk.8y_ ~ŸgO5߼xZǓ'OH)TNVf˧7d6֔ U6kq ʳMMmBYJg(ԣ0nBa#S. pЬ`MR0ܱc;`'箮.|;:t_e;wuuu3sa$$!npK $yd[pvwMC SFBSz/;;;nܹ8=t͛L8wz衇4xZ1kF&ic0T Q}E)++Zޕ[jm6!vxxn߾-YV*<o[f'OWboD9+b-CV4nWN:h(+xo?gϞ2e{FϞ=q1e0PXXXkf؆Qop ""rW^y!!!|>;vleee"77Wdeee [@$$$-[)rssEii0.#q0(Q-̙34hv^nCqq1FY4?3:uꄍ7"??dz>K.ᮻ¦M*O?Tjj*˭GBBȑ#WEGGClӦMsRADT $>s,[L־}{,X@#^+WDǎ1{l|󾾾:u*f̘jիu}Dxx8+B& ())AAA򐓓aÆ9}fc(((@֭ǎC ,BdeeQQQ $!!! Cpp0Ng7c.c ĭsO>ӧK\ޢE ܇AeآADTιsw^$%%.Vc :l6#55HJJBRR2M6hѢu;vRڋA|$''Qަ"22qqqر#Lh }]>QŠAD^E4@Ν;y.שlضZܒ jjr694ضj*A_ QUa,ř3gбcGFΝr]vNjjUCܚaXؚlt:t:???%h 808|]HNN̸8mVffFz^yl6d2)])6PG:Niݐ[8V a1 "/[a,űcЪU+@pwQJؐ}bC!`br=" N y [5AZ8qD Hccc=P{!Oysr ]6l-sЫc JKKq@ Vtw+Qn[7dpڶt\Z4JDuQ%;wBGFFڵkW"44y OkxGaXE@Ѯ];h4\şj48@pia(:l?R Yv-ׯ[n庮UPPPaB+t}4m_ QΓw_Ta{xǰcԯ_WFzzzi999ԩ]ԩ]B:42?؅ry9dȫG@Rh4bΜ9X`F#, 4 `Xp 7 0"::ڣmQ !C^bb ((ݥQ9 7EEE(--UGƍ 7 s+ "H69dfX,Fy $d2F~>(MNJȑ:H{ ^^۷odBppp;… DDζh؆ ^ϠJKKѱcG\xEEE8rRSSg߿iii0صkK%"rtаZX,0L0 JKKVa2P^=vmׯj5$I'ODHHK%"rtА[5F#+++St:XVq4$!::ڍѕ1rldb׉2 0LʠA}zMP Sj:;F*wۑZ5k֠uXb"##aZK/aŊ$ 'N̙3!I/^ عs' ~4hr$HQFA$<X|9?@hhrSbȐ!;wb3l!uECV~ rwâE۷#&&f|?{nlܸ_}>s@vv6^z%4h/2z &O>v޽;1i$,_sѣ'N@\\oV/Ы2%ðADT; ;{5k$I‚ вeKXV,_'OFf&Mի#"{=@@@6lXa9uIзo_|شiN>͛222۴iSHxWW"ڣNh'ᲽW@@T*9 x~QQQQ>ڵ5ϽzjO8{,RSSpBL2{*A*Ɲl'|tD@.\#"":tӧmGE||u{߾}ׯr1|p;v hѢ222}322N6z0hxCСC/ڵCXX^|BFCVmڴƍ~ 7|3BCC1p@رǎCii)Ν^zQW=Ldd$F^ 0fqF`` qw:|x$"ڇAEL&&LPfaDͱyflذ3`޼yطo~c۶mx'jɓ'w_|Ǐ.qPRRMbUj' ӧ4 lٲ>~ܹBhh(݋n~ ~ WJ> (++ã>>clܸ$aŊWuGDD L>.\N¼y3f@Æ СCq 7h4b5Z+4o\6ILL{ /x'qw\wmDDT1h+(\t SLq7o^G`` ykVq)!0iҤUc3gرc?~<кuk%8Qà"~~~裏V+uܱcQQQWrLUҥKѰaC[oVrJ;d""%4\x衇\><ѣ. GN8={8|?5Zp,]pybzY嚈cp ~8q̙sM4q}ƌ>XW~jCDD5h`޼yHMMJWٹsgyv.]V[Vcǎ^$I'xZ{QǠQƎ}h4^sm""=4jHV?/Fw(,,'|rZ,lڴy^C=шΝ;cqơ_~'ܹsNjϣSNŌ3ӥK<.߃Z.\X&թ<СCjϡjm˖-C@@rss1n8HDDAi4,[ * x\ł ,tOii)f͚3gd_)Ξ=;ws~,_ugcШa=z Xt)pit4h&L}y3g"##o>z%<#Bk׮Wl4infs=tIDDA ̙n1GQnu)7*+**ȑ#sNtzZ'Nl?Fqx$IO닢"<#v7^#""&*̉X,0L(--EII  6), q!݊::~~~υ4lyjѐk[+_r0LJW u:Һ!?a;M8E lG!CnB0d԰0wPɭr].Gy+`PJC2aШy.β]6l?""VjBTh[4ؒ~\rDm[:.EDޭ[PTZJS2dm.N Cy?I8ؒ>j+zޥ" rk;ݖ\A\A\A\A\A\A\A\A\A9rFEo8pv5kGyFnݐ;aK}h߾=}eA~@ݑ[Fxx8FƍCfлwoٳa~;K!&&}_ǟX`С8DD?/-E޽ů* D Ă Dzz?~4hBXzj!O={V]VDFF]8p@mV|B!DvĢEDaaԩ믿DZZ:tx7B=Z |X^ZԫWOVWӉ}Ǐ5kֈPa0*,*O#x qw !oD D^^ŤIBNjO?T!DAA"`RW F\xQ!IJeD=999BՊb믋|P!_-+b1~Ʋe˄BF!,Y"F(((NyyyBVt{_|Ezu:ؾ}+֯__u̙3G Bq9@!0`͚5B!y1d!0rM7UJDDׇ]'uLdd$ׯ8qRRRаaC4l۷jEnn.xo0Xv-F3g3f… v jXt).\0L<& iiiZѣrݻwmݺ]퉉^Gzz:zרQ#(]#gΝ;1uT`0`ݺu>|S>DDdA w܁ .(,4k [Fdd$6n܈ 6`Ȑ!1/V?{,~-ЍF#zǏ#11eee9s&BBBhӘ8q SRRQa:@TTFxbz@ѡC_; V&9 Q6mBVV`ժUە^@||<իӧzz|8fΜmۢiӦU4ո\їv͜9St:Ѯ];ѼysuVe[zz B\ѫW/!6l(﯌IIIʾ&LwޢcǎbÆ B!6n(ׯ/ڵk'At:q]wX"^{+BӉ={H, DӦMʕ+B$'' ___,"##C~a>DDt$!ps!$ ^xh׮t:5Bɓ2kyGI&hРc}|| ш\ZgϞE~~>bbbV: t_s""~ ^zƕHulܸ+V@xx8.\ew@t%f͂K}yy1bKODDEKyC~uBDDD.àADDD.àADDD.àADDD.àADDD.àEbbbK7WFDDuiܸ1n* /*A?*"" vyɄzꡤDy. yyy EËhZ< j5FŐADDnàe&L___/&N抈.c׉jٲ%222ТE ddd""آ&ML4ݥQrl?S … _˩2I I "jAC~BXV}fT!lj p'@aZ!bQB(`! JV!IV+Q-Rg2`aZa6+,xEWTT Z ZͰADT٠.`X,F*$$h4B߷Z7j:4lbX,X, %hg`߽ ADd`7d2h4eE!eAC61(--uwY@IIݠPF-"ZΎ & - &IL "ڡN iE )h;v ֭s-//.8zq'Oƀ0|?i0`,YEEEʶ<F?u/yllf "Ed`N<`0J:6}t:tCPP>X,+ϙ3۶m\c~]wK.x믿7lٲ&Lwމ &Î;jʭfjDDlА岝j2jڷ~;K!&&}?Dll,BBB0tP?xb,^Gɓ1sLرO?to;v,:uꄧz iiiP,^]tԩS'j")) $aԨQڵ+x$&&> 3gƍшSN… qIGdgg㥗^B gA\\^| 1m4\cǎm݆8qqqq~ZB^^.^ݻ&M1g=p ?~ş Wi%"}mFM{Wh\[nŞ={+DDDн{w{.Gƍ+n… 1|=z?8 ##~M6$I())A зo_|شiN>͛+Dž(5k Ů8P>dۄ-j9uHLLDzz:z<ߨQ# ''еkkƏ?dddO>AJJ [VVVf͚a駟pYb…2e /^T+))Attt?ś^u2h\5LnDDDC8}|vv6U8p:wy'I&曱{nhʾF}Я_?c8vѼysdff׾}*EDDUS''Xf ѣػw/qbu֡_~Wh4j  ''{0p@رǎCii)Ν^zڴi7*'?nfb`Zqq|qDDDWSh˶mбcG:u ӧOGNCLL ܹsXvwTƊ+5j>< M2L6 M4̛7]vE`` """yfc=;wm۶hذ!0o<3< 6Yfĉѯ_?~v[H?SYKKKQRR!''Æ sya4gϞE~~>bbbVx-受;v QQQvw>.v=''a^Ǚ3gnBPPիW$!!! Cpp0Nw[4Hne(qgjj=&&AAA rcBY(DDD1n0k, """c̞=% :C^ _SlF^iPI ""fkٕ.dZa4XV~'FZ* $A?F9h\'"j @!i{+w,]rАo?n{wP<,!^̢ZZr Gj$yk "*Ooll"jZyԖ"VmX,0>>W4UEv<DdO9Ll "oT!wȃJKKYW*))hHD^#ld``Q%)3GOED,U Z-64Md6a62ȫUECnՐg gFlmq7rа]Zj2YWCҹ=yjTU+k]=q\""gr-ItmCMț9mc6Ul=c$۟!7㝜e4e4e4e4e4e4e4e4e4..~>|5BLL w.]8<ܹsDzSUF Zx1gΜ+.3=j(hҤ_}y 4k׮rA.\|6l={Dll,򐔔5k`ظq#иqc7WMDD .6vXlذxcUg֬Yٳ'Ο?#GbƍP~wy'jt""j`Pڰa@ h߾=f͚غu+vءlkРbccѢE LDDd .4}t@=0x"66ظqΝ;ko^ԩSQPPl<֭>l~:t@ll,uUԩS1uT\p6!njg};vM7݄'x?9DDT7*0͢L抬,"l"DOu @|U>o-A=ߴiS@ڵK4jHݻBq)ϗ$-ZtX|9b ?.]ҥKj駟"!!Z[CHLLDQQJJJwߡ~xw;eQ-Š"AÕ+Ο?}aɒ%xꩧШQ#9s .,[ >(6lP?Fպ\vիZ-1d<~Z "ڍAEWˮ3u -&VBii)"""Pakڴi6lׯ_kf? dm̙|PkQ.Һuk^Gqq1]r ϝ8qЩS'oz!ر=4iÇO>ӧRkQ i۶~zzz9tiӦJ_'88s'Otر'bŋbhذ!nf|WN>^ .ҨQ#?{VÇǪUum9HT222E%'M3g`ƍ1cnF!zSj "ډAÅv ?>VUMMMU۷o+|m;w .Ċ+*lsTcǮz=F޽{7Į]py1o_bpwyZG%Kb+hڴ)j]7..Ta-x뭷s-մiS@ZZZmW*eff"44ʠPY h"H"\tZj/ ܹ3f̘xg0eF}0tPYtjG 7łm²ew|nBMbb"͛W-ZVEff&~ SSS!@ƍ=j/:q3gb˖-زe }]|СbbbpQbÀ}M___9r$x ܹ7t.]/eee뮻C)s=j*_w^!;;gϞ/~#GĸqеkWX,lݺUY?_!"ڋAt:6mڄO?ӧOGNNo+V@vݡCbÆ 5j6lؠܦ^⩧믿n7Xt+]va׮].z|ѣGk<ܹsGm ¼yO85Q#*bd2%%%(((@^^rrr0lذ 32ł'NEp!pI>>xjܸ17n\q0(                L$IV> 燈Z4laѕDDެAC*JJDWV~^$Ib "/HJZ F n&^>o :zYVl޼_|3f z]$gc]# D)@0 IDATBDިJA_bQ„<vLe Q=EEE߿?/Ϛok.,X5k࣏>G}_2n݊e˖!77ƍ{<}ߏ7|F}=Ο?wy˖-1}tJD)nN:T[3f  7܀۷㯿=vEF/вeK|1b]`p_5a4ׯM6!###F`*+44gFzz:q=`HLLtwiDDnǠk9m;vl _w}7RGe`ʔ)HOOߏc[o҈܆c4 w'OڵkrdϞ=x7k.L2O>G|n-j5jO>HHHpw)#Gbʕ*;62\`j\U?E?jG-.jmpBE@* VEK ;s 'u]\br}raccӧ#55!VPB֚qCҥKDEEa۶mXbZ-a~)}͚5qqCI&a޽1bRw:t111̄5,X,C#4BTh( U6aJ ?ǩS0qD9rpEvgܻweeeܹ3fΜ4C#4"urssaaa4VEQQZhDjՊp޽{pssòe0k,éWo޼-[oaE.]Q СCUu7n-h֭[7DEEaƍXf +Ox1vsWBPTۄj',KKKDGGĉ3gNK,AZZ\]]]B!uNxKt /^6T 0Dpp0lmm1j(l!񢤤_`ll#GߡBjѣG1j(+2I&OJŋ(((QCⅆN$`ʕáC Egòe0|pCxʮZVVӧ#99gϞEӦMw.\={___L2|EQAx)RRR0tPC:\WUWW޽{ѿ8;;#33xW'`ee͛7C$!DP@xxx@(JЊ֭[o}ÇG)Νٳg ;4BBGzIE^^^nc|||f|4w#G ** h۶-,Y/_!DQz ...|RkpvvƉ'NL2{쁻;\w8JC믿pm|;wO!DIQKePVPOXFBPPTfMdnnm۶hҤ wS|FQ2 㯜 aԙѣG#..A-K.aΜ9سg(> HKKC1`xxx֭[|FQTh֭[(++C޽hkkcܸq;:eggHϏp!.]4/;]whQ2}²Ftt41F7eyubܹHKKW_}iӦϟ;4BOh®z0 Vdѭ[7ésyyy1b_eA,ѣXv-.]q5IOo{=q+2՟S qeb(((;$'鉻wbڵؼy3:w %%%|GThԳmš?w }Ƅ(/jѨF&Nw(J,/^l3~JNNF;?:!5( C~~>|}}1a%C;zxb /D"ƍ 9r| XhgM;~8e| F۴`4iÆ C^^!5H Ç+++_߿;4B=*4]mRQ׮]ajjpCP(āУG 0Q]Sp)\t w܁VXlC#ѢBCp-||«6еkWBzD5nV͹&m̘1[l!Cw8B˖-qF }ĉq=C#BCAӑ WWWCQ uNxxxСC/qi46m5k -- vvv:t(F8C#BCAAcǎEdd$^zw(Jϟ7w8J&Mh" ܕ+EP4]]]| ;boo,_6m;FG[[gƣG0e|\7_c$Fv)@rr2 Ҹt~GܸqPγg0tP3Nn 'Bˋfu%EC:///*2*puuųgϐw(Juֈ•+W퍲2Cj0vXܼy6mž={Ю];޽|GJBCD>@I&xJ4mW\ӧOW_aÆ!<<Dpp0,,,qF|Q)ThԱ7oBMM z;ا$===={pwwǻwsrr™3g7nk֬ANNߡШc; eccccc\zP<.]`tE`` m-BVVߡԨШC СC0aߡ(5Sv؁/Czz:!}?q]K.={6刐JPQ]SSStԉPĉqq[jϟgggRɴn[lAJJ ѫW/|7HNN;4B uVM˖-ѫW/J={66nWWWk׮EZZ:wrWBШ38vϨ"Zѵz<==?/a``___cРA;v, C#WThԑ˗/};0n8DDD͛7|2 3g`ԩ;R ̙3?~̄?LkTAΝ;v`|C>A ૯BBB֭[m۶cǎ?QRRwx(uRCIIIJJJ`jj5k`ܸq022;<{=z ,!PXX2TVś7o{{{ܹ>7rU޽{Xp!O]]]"D!TCZZZPWWH$BII ?saaa|N>&MQ\\]J,bSNΎ߿?.\ӧOڵkڵkwh9:אL?H$1xJ5nZZZxDZqy0 wwwɓ'Ù3g|BϞ=q1DDD|}}i6XҠPQC ]]]DEEiӦݻwTߟf%*֐H$!JKKUVa…|rOB$ACC|rN8cʽ="NEbزe ̐ X mmm/_&aQZh4 _~Yo)F}稬 3Dyۻw/ rmzzzطoƍo@KLLD^.с3Ξ= PctD]'`okHEEBݻwGtt4/gwWOs{ӧ(--5ȼݼy+N8]ennTT'MGLj"(("5}eQb;bl{U>|ӧO"C^d~QD"|xơyA*ׯ_M6Gii mmm'NbTmתm_?8ng/!~X#Gn޼ {{{yX(OU)))HJJׯ+<{ /^W[z رB?ioKq!)++~4iR@HχP(lٝr(oI2o骠c(Qfٷ+" RZZ2:+;:U({fD9RN7T1o\ުSl1(ط%ըШXV0v2ʑ&6oBqt %ʮ<5nѐK?H$sK9R^7MrlMMZ3JMmjUhHq=X%f.G,r(o͛@ P(:Rst %ʬ6vE*4؊$J);#GySMlބB!QUW1(ըDr,_T.EEE())AII $PM5ycsV`LPjoSN] Hx9ЧOJGii)5kv&??;ȨիAxE(**v2͏sŸqM6hٲ^|t̊dC__]vQLGff&:uss=P*JycUm^c(!UU}[Re/(zP8::iiiry9ѣGW\#]x^~ 777ZGB--->r􄣣#|}}+nŊ\Μ9SvC #֭[W6m777Lžͪ`P_V|8ܤn>vȐ!2aڵ;v,%K|999Xz5УGL:OC<^޿Z]u‡AAMM ::9w{sss%:t耇ƍpttctu>=;;SLe˖رcRg{,a9y IDATzj^KݻwGA߾}CP!HJJ***Bxx8|ikkFHHW$@nn.ݻ=Gm׮]úu}A[[[&8::Çm ]vx݆F+QLLL`gg@l-4ҳ-qGƙ3g`kk+W 77n¼y󐓓wwwĔUVnBNNХKdffbҤIbI '$$HMF HlC@ իW0R_|_5D"VZō'8y$,,,pa9RnL;wDNNomժUPSS{*P7BH]QB?N#// l!YhD"ܾ}[yH#**  !z͛7cΜ9())֭[#GG022CCCau!@]]%%%~:w{ŢzBAA^mG066P>H~P~ʕ+amm 333=Ghh(ܹ#:\offfʕ+9W\/3BJׯ_&r JKKCCCׯ#''G1B sŋC___5N 8{,\֮]+s 8Ƀpcbdd^z A%}d?hh(affN5-,,Gǣ[n2ϛ7x=zT7Q!uEh 444PPP[nAL tuuPÃ;ӧ4imZXX(w`0Faa!===mVnǏ].WWW\~_xwBOO...vnnn7JMK>jٲ%wQE؛DخLMMaee5ɲBJӧUiCCC"UhH_z]חi@A6|(oP^(DEE!22#GDzz:LMMѳgOݱh"@,sJ#՗-[6m|;w,ϟW:;dCׯ_?hii!//ݓ_/rşZl0{쏾mnO?7BH]PBc͚5ƅ Æ cώѣG]]]f͚iӦΆ)f̘QX>|Lk4iΫKGG}EXX]5;ussCPPBBB+СCx!To{f]X,ӧO4|(oAZ]]]|v {6l`xH6={Vs$$$r/UԠ-H;v7oжm[+9|r!z 侞e宒y9EP!&7 -99jjj`4yӎ/_@yp]0gbĈ2E ̙3[6c ~m`~l*˕:vl|ݻ6 w!3qa/yd,^XW^aɒ% qcDy# ͛7qE'5Q :lq=fN*.2d&N899aҥ kc׵ѭ[7ҥKPWWdž #{{{p>6ݝk&gsh۷/ www!&&AAAj077ylnݰyf`Ȑ!D8<233ѣG PHCpB#$$k 0zh //x5<<M6<@Ν;\r\=C'Cy#|311 a4 ThBqqq8rW_a8p:u$0 >H$$$M6pqqQ>z"k׮޽{Abbbp%tRaƍHLLě7oСCa޼y022BBHHLLĦM퍻wP>)SpcXXl;&\VVٳgn;<֯_ &Ƚ,:::+Wѣ `xxxphii!)) 'OqĉJ'孄BTSxx8֮]~a͚5R=gΜ-\7orrr0[h~whiiaÆ HJJBBB~*OH 5kM";;1cF׾}{DEE!--m>}ZxZjӧc`9r$={Ca~Ɗ?]||<ѣJG\\f7E`_]$J{%]AAA%''#..WUL!##Ca(oDu @`Oy`ll n 1AAArˈYY2f͂o.u ooo'O|/Fhh(PisG> GGG8::";;[Q!s!oGn<==__J[bUhGҐ!Cu)"\GGGƈFT{}yyy3grss1bhjjOSSSdeeqgWik׮þ}dbb17XƦ1АխT.^gϞݎ}%0JbWؤ7#tZr]7j 'NNN9s&n݊ h׮~1쥲n: <k֬eлwoO>UziӦݻ7֭[CD޽qk&LPox)4Ѯ];@ddm؃+bO@a Tdeeܟ'Op,%Ix7ߏ_~%%%ؽ{7Ν 6˗AllU+SNѤIaʕXv-޿pt>:ѻwo$&&bɒ%0aq- 0r m®mb\t @y .m|͘1cмysddd ))If;:t@֭-aQވ08w\,^^Bhh(~7=z=† ЬY3;v,?PݻwAFFu놽{a|||0 N<)kpssM!Cwy̘1{FΝ鉿K=z@аaΝ;񁏏nݺ%u0 ggg?;w;861ʛj捔3221sLxxxTrR}}} <ӦM-wku Ǐ?///ٳFE05PZZ0o߾e>}$&&2̱cǘ>ׯ555sY~'3}taC &&&Fj3KKK~cv%OVLll,cffm0 {LYYhтIJJz#G0c%<==j)cǎ1LBB|`444Lpp0w +SVV&lk!AE8q"177g=z$0[Fx=rȲB߾}, %EGGcذaի?lS#(o7B^ hhhU(_eee &}A&M_;n7ylmm!ϼ"sGc0a#F6.-[ĥK"g_*^) ܽ{zzzR+'VURR+Zk媲 YqqqpssC~~>ttt0m4U\1ϫW|-Ӳej۳gOSNRlvB;;;̞=gjjZү_?hii!//ݓB ŋ?NNNёyޏ勽&ЫW/gϞf銑y#PBc͚5ƅ Æ ;eGgddѣ˃T7Co߾޸q;@dK.iӦغu+._{+Wa233]k티0\vj憠 < ۷GhhGC%̌5ȑ#r#&&@LS[cժU???U-߼y8###lٲ1mAڱc޼ymrKbsxe\i?.츴7nU%Yhhٲ%^~yImW#U M5FHC{VimdK>y;v㏵ni7olmmQ\\ ooonVAq};v9?~Faa!ZPϜAe@ǎ/_{fԩ033Czz:dcDĉk~W@@@5Y1RPT3o4wA߸G s޽{ xzzSNHJJɓ'}bʔ)[(b׮]ׯbccm6̝;}vHMM; M`nn^8ꓽ= u 0`T:P~~zL:+V@tt4z*Ν;MMMlڴ[6+… 9s&߿&Mp+F 'OF.]n'OȬj(o(ML (ѬY3t]f---a̙8x n*uY믿((7}tXt)F KKK֭[8q"bccf"޽;?yE2b- lq]C]n7SL q:кuk=zu;Э[7ddd`ɒ%\ acc˗ssW)WTBg2@$ÇCvv6^~  *ٳgHHH@ff&t[[[n"[n0`B& RSSñc`jj CCC@]]b"yyyJ#Vaa!ݻ;wvvvh׮]e~!LLLTT^jjjPSSQTMucԶUَeP}[ZhH@ h0Sm2@wʑ򢼩y.3QVݷ6fս=uuuE9R^7$/oAPjoK+quuuBH ܙ{v,#EySMycsSV :eV}[jDų-PPC!''yyyxD" QRReIհ&MЀP(FPxFySM[u>J}_@P"niP---@,ӌwu@]]]ׇ. BD)G-LP oSAԸ@544 @(BSS(--/I`ND6ewaP\\L9 M5U7oK!'GIDATϦc(ዢmI:a4iRB!5c-Yjkks;.wb؃?7TIUJ}ZԄX,X,ng l%^VVFxlnԄ&ttt\GGG0 C9M5U5og}UAP7Ez[ E6hQRR5/J8LSSHv`+V@}#~PTSuV2C2Pľ-Oh;(V V (ۤ6qijjrMrln(G歺W1MvE *yU2bls}Iꎼɜ؟Hqͱ#~QTSuVt %|R-:L-\l]VV}) WH6K%QM5U5o=c(ᛢmרMHQXDnI~1(Gʃ򦚪<>)r^OEUxY>r\(oy.3":-4!B$њĄBQ*4!0ThB!Da BPA!BB! C!B B!( BQ*4!0ThB!DaHO6quIENDB`circus-0.12.1/docs/source/classical-stack.png000066400000000000000000001061761256046442300211030ustar00rootroot00000000000000PNG  IHDR޹KJ pHYs   IDATxwx;餃Y2J-"DY*Q9 .EA=DD8"e (-~~/iRFې4?וM&wyW!(]7 """r( """r(5w]/Q"""r vC1lC1lC1lC1lC1lC];,GP8ADt B(<,f(= "";J ,nemOUT*P(nڞJ0lA R(}`@=)JBRI0lbra2[5] "RA& :N~c!ARATBVCRɭbaan^ϰAvD5 f3=DD,Ow N "f(R†`pvIbt:4,- "RI,NsvYbJj<5, DDvXvH)D,)ǰADTӕYZ4,68(DD,*}nZ333qE"22R^~aL&i>>>Nj(®eȎҗw7ףSNxWw :uٳgRNC^^^92߇#0lKxwɓ';dF!""`6R0le˖.~:>S:tϟGÆ )S^zΜ93f !?bϞ=HJJBHHz!!!!V&&&bÆ ڵ+ 6`۶m8z(5j:`bؽ{7`2e :uWҳCN%Hf4Eqq"99Y$&&/vP(ĉ'm-[&{~~~8z,ѯ_?x [m'bbv{DŽVBw^ۼz*HHH"99Y,QTT$Fs9 JDTeee}AĉxGPPPzǏDEEaǎW^y7oD>}tR|g4iN: .`Μ9-[믿ĉx ‰'fr-F!">"::k֬7UVX|9"##q ;wwu~ {AFw^ڶmmd2aЭ[7}Z-/^u|ׯG:u6܄BTe"*d2`0Evv62331hРJ?㣲?Dnn.QigHӔ={kFllP&޸qcDȡزADTk׮СCHJJ‰']QŰAD՞hӧq1HJJ±c`6ƍ]vطoK%6Zűc@SNQFFTT&Mp[ ]x'6mXcǎ!33QQQA0fi>>>lF!c *Oĉ6-Att4~i̛7aaa(=ưADUבd*.^HDGG#&&?S%oE " $t7 vna\dٳgmAt:bbbG}ӦMC˖-QoơT*ytT*qk Dt8vU8y$ׯ/wKFHH=I WIT*T*A1l=f3իhӦ qCTTRcV JZmHV˭^[Q!z=Nja0`2]:U2)lzxxQZj!((󃏏<==2Ϯ-DTa7nܰ[q9ɡ_~Aڵ]-I'VZ-x{{Ch4(d iR~R򒃅ԝ®1l38wM7Haa*wW_}ZK.7B!xxxX !j54 z=F#ÆB* &-Sq DdWaa!?n*,w3111nwN7֋5j(i򂏏܅b6\5U6)8XnxyyaGdapen˗/UVVbEGGor`FllKACj0LlpSi߀6,5%F /nш{fL&FFt:zhZ{ w`pCHh4r+!nH-a 6͛7mf ۍ9ӦMFqBDDTZn;.ݪa4j]١jq1ڵ 6 DD.ڷlX V˰ᢊкukܸqyyy8}4N:ѣHIIh]*Y`ߠPNgy* 5kD׮]ѫW/T*( f\p.,0lXtz V\\,h40VjJ%ÝX!1-F(.J`0yK ""rM:lH! *͛_]wqL:}|yٳg1yd͓q6N' ˰ADڪ}ؐ& :eU˗rN>}m۶xyf| 0& gݻ1}{]][5LDTT1 Q;řكT*]Xb4iٌ~+VB1c ( ,Y~b޽?QV-?z(  BCb["00P>֔)SObΜ9NeS:c2 -ӕ;Svv6/^@ٳ9s&/֭[qر=@FF~mԮ]:tqơ{Vo߾=|||0qD,_gƈ#ϟGLLmf͐7nܣG_6EADuˆ]]spp0fΜ BiӦ0X|9&Oĉf5 @IXhuֵz]PGohԨ6h QZRl2&_JOFjj*:v(oLvk֬ƍq:u .ī "YYYÍ3YBWѝapQg\~׮]Cpp0Zj˗/Μ9ػ:ÇѳgOc8{,rrrиqcۦ"""jun#" `pQ'Oɓ'~-Zh5k_~oj!1T*mGFFbǎrƍѥKطoΞ="̙3;wv%""Ǐ.I&2dL&Z-{3ib(˙C]aQc9_t A'NW fz^ҫu7HׯΫT*P(~KBTV lWVZ z::&٥RА~_jZs!j D7ːADܧZ\HWK5L<6VVBp7id2AˉȖB`GT* ˶UrҭRV܎VFy'%#"wV ˠa4j6@QQX` N'QيRT*VRB%"wT!nHg0l^qq<@TsrZ1DwG:ZXͰADBR†`ܖNe` "wUa9m8t:]eFJj<][lHЌFceE䶤3,;;v+'[Z4,_8ȝ{UЈLvp7DT6ː"8m!999999999#++  //2-((ɓ'P(СC[ŋHJJѣGq%#44qqqW^t:$%%Zn __J5==W\4k k.s!:!ѢE[>^""r3F(..YYY"==]$''D yH0h @ӧOR,s1l0ojZ<3Ν=x`_ ///@tEL2_x|_k׮};"!!A$&&d&DQQ0.!؍ lj'ʽ5k@V{6m-ZS]v0o裏͛-Z࣏>ٳ}RRR0}t1xrIDDUÆ 7\*..FaΝ_ܹsqalڴ >>>ؽ{7Ǝ{khw}믿_VB+Wx˥ҥKc( XAAAw]U} ^{ 0}t\~tΝ x7Pn[n?p@4lzk֬@F.{bѢE4L6 0aDDDÆx{{_`ؾ}wY@hh-OGLR||<֭b<ؾ};T*VZ. ի~i%] Z9s@Iذ8x۱ciԩxkttԩ5{`p VZ8,z^,7ol{fΜ_&ͱQ9=*RyaPXn^z5Qưq,]صk<7=JnѱO>])5JVIRR~m%""Z6f͚oӦM+}<|W=dQykV~zx뭷GFϞ=ǏիW+F"":6󟈊BVVx ۴mÇPr͒۽I/\BgTV䌓'O| <<}Z${ZZ6l _,??O=ߏVZs###oVP(W_ 5jڈ)D9Η4L0 (**Baa!rssL 4RN'OĉP*_>:v숚5k:4K  Sj֬ *%U:Gɥ#663rQn"""r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """r( """rr BaGP^Z6,_$J%J6ݎRCU RJܖJJ DDL]KjT*juDdKV- [ȝU(ljk֬͛7|A`0& IDATd2UftV\QF9jE'|}}QF  ((Prr ˠT*VVooot:ƒ;P !D>gPT* ??? SؕBD-@I^T=<<jzFaI]B"q)hKMZfٺADB( eE5JV >>>rlխ[%T+RplÆ-[7UÆJFllKACj0Llp{ϲQ@[Æ@Q""wT1ҧ7BzB`U0p8A:u]BSzF[96,O!'"rGҲQRАZ< NRfMgP-IRWuhG4X""wVJR?I!C>apgPXj8{ l!l hi.['ZxЋ_O}B@Tl6RА,rv Ւeذw4zPJlv(}(h8J̭.Ww%RY؆KDDDŰADDDŰADDDŰADDDŰADDDŰADDDŰADDDŰADDDŰADDDŰQ >}#FŋѵkW# $$FB^^222 ''GwܸqXn >>-[D˖-gaݺuh߾=k.\!? z!W^b޼y>_~0`$l,[L!^?z^K.lRDJJ|nݺo֦^F##ܯ_?}v1{l9\!իW+o߾"66V$Ɓp5[վw^Ǒ:իWr7[ocʔ)Q`ؾ};t:6l؀WKDDaG}ׯ_o Axx84i;vǓO>)dy+W`ӦM1˨ztΝ޽{Q\\3f j)))1._ۭ1//O>996۴j /_Ȁ/BCC| Zl%K@5jVZa_1hР >DDT[n?Xz5v*CbڴiEPP{jjsa6C۶m( :hڴ)4hիW.]VZ̙3vk\v-̙38tbccmׯoߎ4 гgO( /سg݋ڵkޓr Z6c hD-DFĮ]u)))jg@t;3g΄C}5C q񉈈-nZ6j(DDDP DDDP DDDP DDDP DDDP n(""5j@@@#""ɕQuİ>/C~~>\UG nW^/.Q^u'UDDD'rCAAA(,,";;yBDDTز<<<0|p"d* Æ c ""`pSƍ ǏwrEDDT]ō5mhܸ1RSS]USk,3XUc&Lヒ &l6;rS(P(.ʉ-#= B=UYYYhذ!._Zj9TIaCr> *Tf!`2:r<)\HCT߫T*჈\WF.f3F<|0PTPT0 DDU@Bd^PMܙB^Z* B "rq:lX0L0Ltr עj aȵUj|`^Gqq";ߋ4^C ""ưFzZEEE.(,,(VEDTTua`0@ӱeEC`0^x7q&O}b޼yVoǏԩSѷo_|ϗ׭_CŰaðm6=0;V hw5""rM>lHwYj0];rKO'ObܹQ Ʉٳgcݘ>}:7>}m۶xyf|D7{Ƹq /`߾}JAC \[mh'۵kW|wADDmۆ֭[# ĵkK,%K0bL<3f}0i$oٲ>,K/ŋ֭[%Km۶2e +f=z Æ Cv0tPݻ_'y<#_}O+rlDDU Xpܫ7cʕHHHٳ1zhz\t C… qԩS= ##o6j׮1c_FLL |M>SNŪUGApp0Ο?yf͚!;;7n@ド'b={6Fp~ΝsJ vU u{g0aժUصktqHMMMMMEDDj5>={-ٳgF!--j-[Q5dk׮9sBll,۷o6l@Ϟ=NɭV ҬY3t:lڴ o߾߿?ۇgϢsAΝرc܊qFtx'rJf;w}QY8fvލ6mҥK>}:={DDD~\z۷oSNNJ+_|_~SNEqq1NΝvعs'^Ѽysԭ[;w._`ٳ""r Q;95PXX\dgg#33 rXOOOddd@# V\DDD@Ryr={Vg|dffjKOOG5t'R( $$$N:Y&oooh4[>?DD\lpRkCiwݳLJG#"".QjԨawWcDSl8̙32-N[o9"""c9TlXtt^wue;` JQ(vO_&"rw8䷫O/m6`ddrvidlR BuڤaY+MDTY*6,C fwK :N*NUjf%ߴp! `RP(\v"Ty6,i4U}4WOÆ4ҩ2k#rKRА:w=G\Y~~>fh͛1x`޽cǎڵkPml6#,,i5 zG̙tСCv_5`ξeZiٲ%Zn"""۸tΝ x7Pn[n?p@4lzflǔ)S0e\~f?!>//O^)S`ƍ 6ॗ^ƒ>aÆa…j6Ǜ3gf͚Wڬ~:|M M6C=iӦڵkvSNN{=͚5C׮]1ydl{L2?`ժUxG/WQ&h4b%ErrHLL tYV(..б6f'Baa!/^cرcVuyL0K,Bp!"-qE@7бcGWZM4 /`@J[|9`rАaƌ4h?? ={}TTj5F#Μ9c5xM6uqGF۷oNJ+O?aӦM۷"r:9ѻwob߾}.%0lFͱyf]?sLeK.Ÿqjzt5kִ.\ oߺuny@:uiw/// %%/_ZW ɄD]6lJBAAT*N<ɰADna6"##(,,uӧ~RSS{wC 3JBPaa!T>.771P(PXXh{Q0 ˃gȝqrBq۷᫯(dqmIZzq%޽?6֭Cjj*zp%gfCad_QQ6o ooo۬W*裏0gt:zxyyY<==˽<{xx8ᙢe˖.* mmÇի1| 2enpB33$ 4@FF.^-ZXst^pp0qUܹ&l\rÆ NÉ'P^=ԪU YYYXv-yc߿дiSyݙ'كx?Ao* _|y%]ZjYAAnܸQfSÎ۵fZ ׯ֭['c=fm۰uVmVmi֭ضmbcc1h %>Sx{{cԩ8~86n܈m۶:u+ !?bϞ=HJJBHHz!!!!6Ϛ5 xQPP{'}oiӦٽ:|MԪU ś^ǘ1c^wSRR0|$''ƍ@tt4^y},[lupQ ** }Qlo8kעSN={Į]*o>^'|"߿ ,SO=e7l۷ ,1c䰑wyAAAS^x_V?ls~vv6F?V̜9 ޽պxddd`Ȑ!lˑM⥗^Ă (y]*((䰱qF 4z^~//Xj֭[gXz=ME(i5T(8}4֮]+Wb͚5V]X`FǏ>Pҽ)9rq׮]6cظѣqeo7Ftt4>;wǎCVV"""?ۄxDZ~z^/^ߏGСCAFF\1vXZ Bǎ1`4hwZ+WO4 +VSбcG :͛7322йsg9a5W-Z@||<>3|w>}::v첬T*Ѹ%G1  ;ZVn݆v IDAT)kb]vUVGa0믿b̘1X`:wVrssc0aZnD{8~8cǎYaϞ=§~/^ĪU駟O>غu+ufs><_-AaŊHHH6~s=Wȑ#bȑӧ~3¢E={J1n8ڵ G͛moΝp|A?]]t;#$%%^Ûoyw<ӎV|322NhSO=%Bt]4lgV0@\rE"##C^[4zll+/..=P*V5lRlٲyyybvN^\>w*:pur^~a@Xfݻ+#G,+Ę1ce/z>ϟXt|ڵhԨϷI& 'ZޠA@\!11QJR\zj]ZZP(BRY~۱c 7n,f1SRRB\zU^sNqE Y^.&L)SȯS7n@@t>+0lCnnطo˗/7oCƝjСCb˖-?Ž/ņ ĩSd.]7n׿Į]DNN=ԽT^uYO>`999P^VѰ~zM0AC ?ϑ#G!Rؘ5k>fYҗ_~in޼yv?۶m?ݻ> DNN$}y>!xO)l \hBP (XƖkOOO,~;޾aÆhذ+"^~i۷?3ׯ{޽;j֬Yj޼-in8unjߘ"VӧOۼ{mT(xg#!!rRʨQn|A4m)))ҥ z! 0ݺuC۶mmΝ;իWǼeMN&OإKl׮-GuİADǏl{gL|/RЩS'?#FjNJM]'%tU%gLr˳YV֤~>,}$&&ƍ]6Ο?#G ((qqq/ܹcƌ֭[k.ymz}Eq첅& B+"X0F zF &=Q?M&c,(bEP5lD)*M؝|g.J]Kٝy991vX|g`vRw Ą5j@fjPA!<裏Ν;h\rqqqý{|Z;1 f>DcPnҥC @6i߾=|}}cǎ>&LuNNN8s p)\pQQQ믿"11Qvbiiij?1!{ W_‚PA!D8p wWG^^>#0k֬zlL6 񈈈|PPy뭷0sL̜9 ** #FG~,\:t@TT %&&Ą섇x)TagV&!D222`ccTVV*=goo-[@ ŋ((("JԊ ޸LG={tIIMMEӦMwQ[ǏT*ETTbccq jٳacc[*=.r!r _~uxU*6!Dǜ!cǎ<-0 -[?׮]׹m)St' 33S-[ 99:u*e111vk0w\bH$u5 y`(k׮(**®]T1r97[n6oXvZ %%%pqqPŋ+n*+""BƎQ!+Vܹs2 .]ǹmXnnnprrÇѿ :0b>ٳ' .] >>j۶mhٲ%1i$ݻ>>>2e \]]pddd}ꫯO6 sD"Qՙ1cHNN&M'''\~HIIA1qDUwmذӧO.]SNA"믿???xбcG ,, Æ S;IXVe< DW7LƬ\155U0I&̆ T^tEi[槟~b13gr۳lB͛LNTqꫯT&!455eBCC<װl$%%g`5k`FYv&'z*v3nRySN1*۷jՊIHHPM2 0Æ SyϹs2<䄆z0L?2 ())Aqq1ObܸquZœD  "",--aff;c\z߇@ @vгgODl\zvvvpss233PȍYHOOG\\[[WXXܹs}E\Ǐ舎;rӉSVV7nիBѾ}_!11~m4P!ԍB!*++0 l4@rr2ڶm cX~=&O ?Dxx8ك>VVVZؖ 0339,,,`ffƵnQA1f)6$RX,V)4r9ronܸ???tqZh֭[#;;^^^Zp|Xb7o}A (eJ0559`jj DB3416WO rb !hF|7nUօ1b1l0gΜ9x뭷l2lܸڵt]Zb+¥R)RR7 }bs4h[p0 T}/ J!ɸ*6&&&>>>hժ5fر"M4꾦O-[O>~=zhul [p-thLh! q  0lvu4̹sPF~`ohٲ%ΝM6O6;LoqeV D|8??X'նM'}=14v4nɓ'hݺ5ߡc*UafDDD`ѢEH$:ۧD"APP<:ۯ0d`͚5:۷ɀTyt>3ƹz4>ɔN ?:?NXtN䄸8 :yyy6!D׮]y/V]+ ֭[aiiw(zØn={;M6x^ üypQ1b鑽DGH?~ ///}p-!ݑbhR/=QRR-[;zڶm,X`Ϟ=Xp!~WՋp!77752Բ'~WG Àp!Cɓ'cΝ>|^wB-Z@(";;PQ' e(])aÆرc4iΝ;|CHF-ƅ~-w( StYYY;矱h"}nnn|qEEE>|8ڴi~oY&DѴƃZ6t,..M65B0ϟGaa!QZZwH5XN:ƍ|B 3.֔)Sw^MLMMqc(,,;$Bu*6tH&!<<ܨBK.hѢ.\w(Z#k.x{{_~x1!bhqbCжm[iӆPX*شi&M___ܽ{1JԲah͘1o6͛w(Zt999033;KԩS;BӧOѱcG ijБW^ѣ?~<ߡh]fЧOM>ر;BR)=zw(e˖|+EѣqA7G;B u>*6tBnHHH@nn.ߡ̀pY̙3;w;B 5|Tl@qq1Μ9cΘa̘1طoߡ蔇bbb_`Æ |CQ Gņ;v -ߡɓgй: ..wƧ~Jӛ@Tl>*6tu\9::~C\\@&!sHOOGYYߡzbC p% BL4Q UԴiS?9993f ( 'Qe?,--SN/hqq4iC AQQ!b+ŰQe յkW888ŋ|H={oTw)tGabCrrraÆ ۜ|w7n|||p}C"ĠPˆaʵhHII]W999ҥ o?`ժU8}4@Ӗ6jТޅjѢzcǎ^5k; <|CAppp)233Z{ϏPBcs&ƍ1f8qp1ԕbВ07"P{gX޽p{4HpQ%ԅFBXXߡOOO`ٲeF- -u={}^Rԩ?bɒ%|CޢbpQGpp0ߡ???}pu*64С 2J IDAT;111;1d8qӧO޽{) .ȑ#i:ݻ7.^%K`|C/ePQOϟ?G=yf<|ǎˠ])YYYXn DoB.]m۶aٲe|Cι 11[l 0uTC"o@wSZZ<==!PQQ^cǎ 齗/_"<< .˗/! QVVLjC@@<==}vpBOxb1r9`kkgϞy :;ՓT* JJJPQQl̛7M6Ņ OhҤ >cիW(++Yx"/իW@Νi]bn޼/^%%%yc|fffJSQ?@zz:QVVb,XѨ-Z`ʕ355;QڠbRRafft҅Ǩ ǰaðvZsQQCnnۏ?Ʊcx[`KKK5PQO-Xjxʰ̟?'N Q?GmPRRxqq1>ÇHݻws-fffJO:ŘNz2 bfff3fտs$燤$P27&Ν;vZ<~eee\!laa]vaرZݿ6c:6ѣq)ٓ'])6Խ10D"~mC"|?9z%CTVV.h3o;[9r[ ?.y}Ǐu֘5kow8Ow/0WM<8p3gi.B@K}QII ~L2(Duϟc޽C:tCO85o9)7hC_ h׮ߡ$]ܾZl_ v|{`B/1H?Z_m&t%|ͪwdW&MьϟC$A$wpP0)M$][t%Ƿ"QCa? dx8=vtVH?Q SpykAQo4u|UlT+++QQQMKSVVDa C&Q0yn211kkA[z>$z;(<#Gy3LlD"7p]Zy賆ߊx5rTTTke(Gfؼ;r2(w 9k(lS/{ԦLdggC W^5nJ١}jy9nݺԩlllOYY.]XP^^2jbr9\hݺ5ntJ[n簴D׮]͛7Ν;ɩ^-Rؓq]u}%4q|{[oW+QQQF޽dgg 8ooox{{#??^<}zbZYY41.r  7,YRv˗/pĉX}u$VP)b{!''G۷o###˕bH1Ɩ/0[} >rMh0@CzU\.ǹsg{XTχL&UXojbs3zhhHKKSَ}mǎѪU+-E/ʛa~.KS3QBj!wu FQ6͛7QXX\rr2={''',Xpy͊P5QIba_ &`|clgΜAHHw|xYP}?gAAVZxxx`_j8VP/!|j(|8p A||< =Ǟ₎;Ν;r ]7774k֌{  .TXX+WbܸqHIIAAA.\WWWdeeaJ ;NMMU(66%%%֭B(ҥKJ $$$aBۋϟ?ǣ+W=z8pF6۷?3ץrJΝ;07B&\QTTDD"yP,6JJJ(Pu '''a̝;ؼyژpAxxx @LL ?/@&&&˗ǫٳ'JKKq%n;6hڴ)AK| "WXmۢy DLL ,--WdbbhL:͛7G۶mb V+V4)oM2b8Ѩ7 T*˗QPPHs7-ZKKK}N>pIYF1{{{u'={|wbK]K~(\~͛7G}Ogggj_~Qy~„ pssSy|x1޽[Q!dpc6_~(--EJJ zr۷/qqW\zB&MmDvc(++ß 9 kNm{ܿ?˗Ǐq5XXX_~v/M6)]U+Brtt>sunЦMܹs߯ƂF,6,--ѫW/!..b:FEE!22RP< >y򄻳%44W)@Չ4h֭[rn^B"p۱R7oģGRHRۗ?C%U|ݹsQEy#hA@UX1ppp@= g}HrWbCqҥKѺuK.Jggg8W<ӧR)p M6ٳgGi 6_ݻwǜ9s^oDzjܞ7!DS Xz5q!CTೣ333"+upppYKqq1N\ĮbLSו|}}qo\[>|ܴiر#?>!!! 6®N.ÇwX7BQ^q Xj1g'OThڴ)u;oےm6]vjWesxoQfs$  ;ծy)e˖5P!`ņD"ݺu b}ne˖*]\QQΝB >\PsrKnmۆd4o|A~@#Ԕ+gggt ͅ8p 7`۶m*3:t@YYƌ[naѢEJ=y/|jsQވqYYsrr4QFNl?l؉yƤIw^`ʔ)puu_222о}{|W*eۭ[7 0nnnHNNƹs`bb7*]7Fn`@@dP֭[닽{޽{paUs9899 | "##1x`ʂjG6 7b,>S\x\k޽{pBѣ 6`ĉjoNJ+Tl?~ƍ9JHKKѣG{n9rz{+!uEY}ի ĉ'hnQPP\pA>3Jظq#ҐO>SZ%u?iӦիWXv-PZZt2e #j swŜ9suVልU=<<L-[`؝'O`ǎ1{M6_|HHNNFAAxb.6ggg8q_ haeBYYYaÆ *w }7Eqӧ]gΝI+44nHHHPz999(,,Z6!\nleeeܤ)7onVꬭѾ}{\zqmڴзo_FQ뢅(bBΩ ɓ'ܪo|vf{ڙgYZUann/b֬YĥKp%@1vX|g4/GQA!D/(Nt7vU+׸/jΜ9,:u .\@TTrss}vHLLd B!zχf͚UuqqqxA<|[9s&fΜ a#FѣGؿ?.\Xlx $\|%''#)) wޭqϟ#)) III\6hl;rgήq\nu>VmBRR233}Gy# @PuRSSѴiS8;;s`9|4UVٳacc[*=.0x`222j~ /FTTѻw+lxzz5ׁ oook+dndc& ooo,Y/_Oģh./[#1T˖-ڵkJaܹ(,,!H3g΄rrr׳?^]vR閑nݺgkx)6g~igrOKKãGnǾm۶`ŮIqGAU⊽D;(o <&MByy9|||bOѽ{wƢ}ꫯװ1h ^K.n߾^zj3f̀c ///DGG}8qV~~cKbccnÞbձ{۷JPTkʗQވڽ{7+TTT?͛7"77HLLTe8t4i .`ŊXf ^|/k׮ƕ-,,pqxyyXx1&Nu!%%Bjۤ^8w3@}D'AySKѣGEDZZvk;vVZi)Z¢}apԩ Xh>3nV}Vk |f1:x4U2s2"DLaa!0 sImW\\̈bsq 2???F&쓽swwWzmUfOAe4iqrrb޽֭ټy{3 &551 x\PP]^p{O?e0~)LϞ=rrrŋUy}ښ>|{:̙3J~Ƚr|13zZP*7j Ʀwu^u]|"::5`J|2Hs`}u'O\v O44:u1JL<{*# IDATE6mv)mׯyj_~1={|wĶ>)_%}b={Kxzz*զM7`XQMF*ǐ!CPTTUV/T٦>L%>Q 3o^~A,U Zo߾dwJ^ФIUwxMwwwDU߾}[y>>033Sy勽> رcz^zɓطo]eҐP 3o(6V^x9s0dTv|ff&QTTsss.:={/_ǽr wΝ-6oތƍXnVXm^eee!$$y333… ߸xu78|0"##QYY @u: ** $w 'W}jG}-[`2dICBy#077Gnn.{W\,vUVآмysnf}Uj3ALul䄕+W֮]&a^^ԾOpp0lllw9>-I۶mC^^ڵkM=vy>tګJ\rԖb_O>󕶫*fy#X^lH$t- qv`.ŋ:IU_8##۶mnpW^!$$=t\mTNȇPVV1c4(]cljʕ3:u| ^^^JL>͛7Gzz:֮]VBII \\\0iҤmeeow^ʤf1wU'BÃS:v!bcܹعs'nݺ޽{#((;wFZZ=,bڴi [$G>}-[`޼y[{Ž{ $$$NNN C<==aee {؀nlف+ 6`X|91`Kp)H$|ba # gΜ~7oI&*{LWWW.##CeICCy#Io VMW\@U?'._ ;;;*HR$%%?ľ}yfgϞoV#'Aj̙3??G`` \\\н{w`ҤIHLLTƺux>b59~8$ҬsD5݊Ÿq.ćG!55YYYpuu;7Qrܾ})))(//G۶mѿL&$X[[ &&&())AQQUVV7nիBѾ}{esS=uAyk8fkk +++XZZ DE:C9ƩwuzѲ-Z}IlP.]Эwo`jj OOOxzzʤf7BHm>@B!ƍ B!hBѪzFZ1Q n0P ;پ·5eCqBh6dB;(#Ey3LVk4q|sՐߺ C4&&&jxQ0[]y+M߬zZ711H$RzH$⮎+$`/ʛaRLy賆JU _uD"D"߿(**‹/PRR2TTTvIjR;\,C*M456m X HTf꒷tyIǷzPHkffrnyjHT rOLLL~ߖ077)R)wD"fj7HKS3G>]]f\bX0 D"$ ^zJh{rbO>l5~({p0 W^QxBy3Lu[}5011D"\.\.g? l5.ɨfWDD333wnffG fj7+ڢ(6oE VD쉑 pTTTṕ胢I$*e؃ZB!fꚷڞ v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} CURDIR = os.path.dirname(__file__) sidebars = [] for f in os.listdir(CURDIR): name, ext = os.path.splitext(f) if ext != '.rst': continue sidebars.append((name, 'indexsidebar.html')) html_sidebars = dict(sidebars) # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Circusdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Circus.tex', u'Circus Documentation', u'Mozilla Foundation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'circus', u'Circus Documentation', [u'Mozilla Foundation', u'Benoit Chesneau'], 1), ('man/circusd', 'circusd', u'the circus daemon', [u'David Douard'], 1), ('man/circusctl', 'circusctl', u'circus daemon control insterface', [u'David Douard'], 1), ('man/circusd-stats', 'circusd-stats', u'circus daemon stats aggregator', [u'David Douard'], 1), ('man/circus-top', 'circus-top', u'display Circus processes', [u'David Douard'], 1), ('man/circus-plugin', 'circus-plugin', u'execute a Circus plugin', [u'David Douard'], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Circus', u'Circus Documentation', u'Mozilla Foundation', 'Circus', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' circus-0.12.1/docs/source/contributing.rst000066400000000000000000000064471256046442300205750ustar00rootroot00000000000000.. _contribs: Contributing to Circus ###################### Circus has been started at Mozilla but its goal is not to stay only there. We're trying to build a tool that's useful for others, and easily extensible. We really are open to any contributions, in the form of code, documentation, discussions, feature proposal etc. You can start a topic in our mailing list : http://tech.groups.yahoo.com/group/circus-dev/ Or add an issue in our `bug tracker `_ Fixing typos and enhancing the documentation ============================================ It's totally possible that your eyes are bleeding while reading this half-english half-french documentation, don't hesitate to contribute any rephrasing / enhancement on the form in the documentation. You probably don't even need to understand how Circus works under the hood to do that. Adding new features =================== New features are of course very much appreciated. If you have the need and the time to work on new features, adding them to Circus shouldn't be that complicated. We tried very hard to have a clean and understandable API, hope it serves the purpose. You will need to add documentation and tests alongside with the code of the new feature. Otherwise we'll not be able to accept the patch. How to submit your changes ========================== We're using git as a DVCS. The best way to propose changes is to create a branch on your side (via `git checkout -b branchname`) and commit your changes there. Once you have something ready for prime-time, issue a pull request against this branch. We are following this model to allow to have low coupling between the features you are proposing. For instance, we can accept one pull request while still being in discussion for another one. Before proposing your changes, double check that they are not breaking anything! You can use the `tox` command to ensure this, it will run the testsuite under the different supported python versions. Please use : http://issue2pr.herokuapp.com/ to reference a commit to an existing circus issue, if any. Avoiding merge commits ====================== Avoiding merge commits allows to have a clean and readable history. To do so, instead of doing "git pull" and letting git handling the merges for you, using git pull --rebase will put your changes after the changes that are commited in the branch, or when working on master. That is, for us core developers, it's not possible anymore to use the handy github green button on pull requests if developers didn't rebased their work themselves or if we wait too much time between the request and the actual merge. Instead, the flow looks like this:: git remote add name repo-url git fetch name git checkout feature-branch git rebase master # check that everything is working properly and then merge on master git checkout master git merge feature-branch Discussing ========== If you find yourself in need of any help while looking at the code of Circus, you can go and find us on irc at `#circus-tent on irc.freenode.org `_ (or if you don't have any IRC client, use `the webchat `_) You can also start a thread in our mailing list - http://tech.groups.yahoo.com/group/circus-dev circus-0.12.1/docs/source/copyright.rst000066400000000000000000000017051256046442300200660ustar00rootroot00000000000000Copyright ######### Circus was initiated by Tarek Ziade and is licenced under APLv2 Benoit Chesneau was an early contributor and did many things, like most of the circus.commands work. Licence ======= :: Copyright 2012 - Mozilla Foundation Copyright 2012 - Benoit Chesneau Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Contributors ============ See the full list at https://github.com/circus-tent/circus/blob/master/CONTRIBUTORS.txt circus-0.12.1/docs/source/design/000077500000000000000000000000001256046442300165725ustar00rootroot00000000000000circus-0.12.1/docs/source/design/architecture.rst000066400000000000000000000036231256046442300220120ustar00rootroot00000000000000.. _design: Overall architecture #################### .. image:: circus-architecture.png :align: center Circus is composed of a main process called **circusd** which takes care of running all the processes. Each process managed by Circus is a child process of **circusd**. Processes are organized in groups called **watchers**. A **watcher** is basically a command **circusd** runs on your system, and for each command you can configure how many processes you want to run. The concept of *watcher* is useful when you want to manage all the processes running the same command -- like restart them, etc. **circusd** binds two ZeroMQ sockets: - **REQ/REP** -- a socket used to control **circusd** using json-based *commands*. - **PUB/SUB** -- a socket where **circusd** publishes events, like when a process is started or stopped. .. note:: Despite its name, ZeroMQ is not a queue management system. Think of it as an inter-process communication (IPC) library. Another process called **circusd-stats** is run by **circusd** when the option is activated. **circusd-stats**'s job is to publish CPU/Memory usage statistics in a dedicated **PUB/SUB** channel. This specialized channel is used by **circus-top** and **circus-httpd** to display a live stream of the activity. **circus-top** is a console script that mimics **top** to display all the CPU and Memory usage of the processes managed by Circus. **circus-httpd** is the web managment interface that will let you interact with Circus. It displays a live stream using web sockets and the **circusd-stats** channel, but also let you interact with **circusd** via its **REQ/REP** channel. Last but not least, **circusctl** is a command-line tool that let you drive **circusd** via its **REQ/REP** channel. You can also have plugins that subscribe to **circusd**'s **PUB/SUB** channel and let you send commands to the **REQ/REP** channel like **circusctl** would. circus-0.12.1/docs/source/design/circus-architecture.png000066400000000000000000001045231256046442300232550ustar00rootroot00000000000000PNG  IHDRAJx pHYs   IDATxy\TU?ϰ)K咹Re s7ωٱJL3:c/ L  ׅˠR+H JMDY YL 0 qk \Ld2焭q*X ŅD"^s+&#+`րz=meBt:O./p)T Ƙ[dfs–{~q$|t65p ={FJ0f9a5Vq>\kKA fwe [UJjmi"dggW.`0]8}C~Nw/BhZt:zz FA~Nw/UmƒzVv.Dnn.򐟟/ [(q?7a40yp|B_\ GS  u׸s 9a+}Xjj*gL.sΙ3\ΒsαVZ ҬPۼyE۩S'&&H\.g۶m㯧]2D§͖,Yx`lѢEl͚5nݺ|5klƍl߾}ԩS,11ݹs=y2`9G9cѣ?;ucL|{رcQzu OD޽ɓ'EwVXpO>}pT\۶mCfУGԯ_@A+YfxF:ضm>#$&&bڵ Cz ֯_۷o_~7nN:;w[n8x :wlODKٳgx wb9,X ,@ǎ1h СCشiz3gXϟǠAPV-̚5 Xf >~RIqRDD-[ L8ʕtԉeuWʢ ; 9s ||_}U>ν{^ٳgkL. vAڍ7fؙ3gc 0(&ܯ_? oܸ`xW^e[gظq?f/H$Ì1?~+fh4ZdSIkcxx8?cvLV AaÆ1,99-pBUV1?ÊET*ejZ-ԩ@->7=z~ * g\0` ???Қ?>+WD~~>&LU Λ7{KmӧOCBBpe( ?ƦM J1b511l߾]pnݺ?~ l̘1_>pƍ2D[(\QQQ4ht邞={[nxQB>ޜ9s{nwG)P3'--6?'/Yܖ-[in WkԨUTTni]'N`ٲeضmV\+WB&gϞBݺusΙǞMڵ+:WPnݺEƯ]6 ##{H?K~X~=bbbV"""5u)))f޳gm?\Y\R HuuM^:k׮իWk֬iӦVZسgz5jݻf-[S +\ a:&]\@8{=\"`̘18unܸu֡o߾x!Lh|ڴifi Kk^x,n~~>U0ɓ'jhذ!ڶm[ի8tٵ(ԬY?3@R4EכmݺQQQ0 Pҥ /^˗/N:q!PJ2#Ga_.2C"9G{[?y&-[Ǐ(l5 vŠ++W9Av?766,nBBA8խ[[VŁV^xwLEpm^T'O 6 ޷oLÇ r9j5XeՀ:uiӦ!{B.cԨQkN±ca;wŋѶm[x{{ha_JJ4~n4H{rf7fK8,Y]vExx8>bǎعs'̺L,W^ѣߏ>}`ذas֯_bܸqQCٳg#335jÇ/^z$F¦M0l0,XM6Ejj*~\|ݺu3]W\!}va ҥKꫯb={6Zj۷ocոz*ƏAZRĸqТE ={/Fợq)?+ypp0^y|UZUDƍّ#G8[שS"jlȑPJ%Ynn _ |lڵlf뛾[+իwݿ[n*U$H[*y qaÆ[`AAAlb puIsKu|[nΝ;y XttElsKa߿_ڹEFF۷yVoJ c?h4"//:Ϟ=CVV233#f)2p 88!!!6ې:33.\JBŋGFE#$$$ %%˗GzаaRvt:ԪUK0[ŋHNNFpp07n w݋>}`Xl>}Ǐ-ZyD"ƍ *\r4  n"(?,G{[?x\v ]65kVr\0 ZeW"..hܸqK+]aaaf7l:uTb £pN4i6l(pe K s3WÇmA?a IAx$|AGAGAx$|AGAGAx$|AGQf3=DҞJOQ9ak9 V T*D"_!J/S_pu ?'l;Wµa{fN8?E2/9aK=Vh.r9d2 +P&+\[JdP(رc?~ dee!;;Zyyy06=6;#Z92 * >>>ErT*l*xrrrÿ={];{R<5Vr * * j1d2@`!$$UVJj  rl]( ^{tb'ѷo_T\فOi;:W_}jZlSDc.  4 `4JN`hNݻw]םDjj^^^Zk5YweGaeIR ؟lƍh۶戂'WgjyyyPPQ*C~~>Z222c<~٨Y& n>ed///T*>3:eˣe˖8sL9d4o֏0~m|4xj~q>ӚR`` w2<9r"Zc[L-̬Vh F#p1 k={q222VZl*X5ŴVcڒj^^^€ qW\¿ӧO*EôWUTP(A{WyJгgOٳǢ <Æ 8L$%%:޽{h8xb~q%lcAPפR)J%z0 w?a۷o.E˔kq5ZnyFF,O~ܢرcŋ#ԭ[WDW&:d\.gyKSAءCCY*U 6lDGGcƍ.1nݺan0ѣfa;wĂ DF\<98;f1 (r;ߊ+h"cʕ"Xd[ i3] ^.R?ڵkj*JD")۴i͛7Ŵ,4nz^VqTPA$'W[R)F} hcǎk.̜9L3&-SFY999Xn.]Ǐ S… o5jm۶سg@nn.~W|"X%_\Z|\}38U8Z-Wwy֭sU𾕅3;U\]-BZZ nHH"##T^z#66eŐ!CoyFHLLtřRTr.|5L$;*R8C{yyyq]ڵkc>|Y!?c ,^8v.P/B_~o@O/N #fʔ) s_cƌLJz=[~=Y[F rJW=zĪWΟ?@ïZbԩfc1<&MŋύT*q5T^AV`0`͘3g_.VjUL6 >Jei=y2U4~ml޼xhҤ,"f+~t}F۶mCÆ D/00K,k0i$D[^^^vmQoDɐYɟiqܵkv1رM4СC_ … q DDD2k?iQܟ~l+A8>+9p@q$ ?Veeh֬K.˗ܹsq DFFZ#'}vjZ]֎Do1T\+WT*kլYǎCPP]a8߿3gٳg~~~4i>#Yogk&NNoooh4b… * [l|||퍀ZN8$|6PPhp.bbb0sL;R999h4"[Dȑ#OqAFᅬHTTI$\Ξ=^{ ڵL0R(3f ^+Vbbÿ p$$|vwxѪU+r#F@rr2֬YC >™<>jϥK0o\"`7o5j$g9[naF~~Z>}0|4kL$<>™!4ܹs-ڵk։u}Zj%u ̐jُ`Xz5rss:w> ۷:g _ZZct겕/Ċ+#ֱcG̛7;w8 j P(_<222'ؿ(O<ҥK|rM6;w.z!uDq \2222tw𕎧ObXt&͛7ܹskdQ$|3C; +Xh1{l5i;vsH#j Y:rrrrJ|HOO\kРfϞX@->™!Ⳍ\|wXp!;Aۖ=<_ ,;wׂ1sL{d"YHX ̐ jM~~>6n܈֭[k5j30rH( ,$l b0O?aܹ~ZժU1c@Td!aKG83$|v&`4m6̙3ɂk6mƏϟM$|3Cg'<Î;0k,\|Yp- 1&N///,$ H)0PT:cvڅYf!>>^^|yDDD>H ;;I T@sȾ}ЪU+_ z~~~O3gyjE !#yAk{ٳgpooo|'q͛rʉh%Hd2?h4Rp*h  j_5q=ܿ8wKq>|f‘#Gƍ'|J*d!6>>>RlA 8x L‹h\۷oﲢwI̚5  \Ra̘1>}Kx&pVHlD4wٞgb֬Yطo \P`ԨQ1cW.uA3; glJlqW_}Վؖ8Z\.ȑ#qZD@G8+$|6d̘1Q\9n-..]7|v IDAT͛7Ǯ]pLaÆ!11ׯGZe. BDDDv*ˌ19_~s%%%!<<M4 nT*СCq% qلBc|BgcƏ *<7XݜSL? ?6~5 >/lOБH$x7-[^z6pAG8+$|6~s!|/W_}n޼ѣG~a0k}}v4jv ᬐف'ϯkkFhhCY~="##a| 0~xaݺujУG>}v‹/P{ pVH@1q"9zocǚ?{ ͛7ǪULJwǏUV4p3hpVHɓÑݜGСC]w#kᬐى7N&JѵkW?!!Cnns8r:uπpVHG}$دe˖%7n@=fddAv?#^z%SlNCq…RWc޼yXn, DR{u!#FAxN2wcLtVF8tPq V5jz_UF V #\>YJL1ƋT\ ,NKSLSвeKT^ժU3X˾pvHRD"T*7K4G8+VfqBg0\pUB|233!! H$~ÇJ*VlA`u<&` cBz$uuΊUg t:K\./pMT  VF'K(NX1 χ^'9xx*R*Ƨ ݍgϞ KRFtQ$ |}}233ݼ I[|1{r*V zaX1âػfo}_wlDðoglABuxy84bnxm9ZNf~YNdhcO^~ ڢY:Fպ1n.nkiZ[fF|NDۘ2ޢ W(Ӵl[o}кd^G=~&]A&{f?ŸlCǣn˗ٙ?FxeNזdggB'a0.OtMhpF>n|:_ha¼%x|*TYt_| (>3^;KG/C[^3{7sD6a^- #fSr2ϿLvn ͻi~bzisԈ?WBTBTBC.h4zOwpFl:E.PNA 0Aj0R >T,QGFv0h4J9e \D&Y֐ F| ^H\ZG8#e< #nh4>g_$=]3]=?7qq4ԷԄOJc~d:(僪nۉqkF4}u(*k-:WH\jΈM,3?gŸ;p'9~SzZr+9ݙks>c _{^]:4@E^H$Wh-!(>ZuFQuo[8ۗ|d#?OrUGKJזz~ {@G8#V/`7mwҷFH“HP>^yw NoU͂\Qpڂ63pzǛ|U&=70 \9sWџWϫ?[>ñDfDpӗ<?ׅg L #W-|*TBnv&S]wRAJ%SvڅcE^r0* j5n]l|T; ?^ DԤxon'AY,;(ω?JjṚOBG8#V +;ڼPkPaI:\ڍybhrvSTAkI]A6'4g{I|bQ\GPا\ٷHgfѺ" uGhH$Ṙ׮No6uv~OͿuhm~g\<;M+~t鏠>] t%BwRۉSC e z)t*6{Ըz0v,M^ { ҿw /3~l[ h|$|3'W(7.b0-7-^cffihBF吆3az<.IN@hڢP;- aQF fW8ZT.G/_K1S*54m+PVQz$%g: gg"H$抰 }6vmQ*rn 덵X-|"H"wp=i^{LaLIW'[`8KRӍ8+Nи1}s9 YVX7kBpMssuAL}D//",'33r2 1{LYʇ2 it1#JK9.tj0UD"^/d>K &x,c|udt%J+T&+㠾q8sjuQTJ_?z=t:l lógϠo2WeMwЧVa-֦\H\???ӧO~c8-Sj<^s;>NiiV'a bck"8LN}dggCBI ha #=g::]Nz=z= [|$v1,e֞k#v0ܢE(;\7g~~@&|K##lbFw}>r+,VZp|~|O=nE=AXWEm[F&`&~kuoZabӅa-c|yˏ[N-z*] /eu|57C*U3LԄk`ڽishWgQP-[ek#sxnAOo"|O-jFddd~HMMŃ6 3).%|׮]?^uM uE^l`@vvՋtZ-vLX->ܹsm,\su\J8x |}}1ydyW닇2T;x ̙?ClSƱc˗/ ?3̙3iHu) Enn.#)AX _jвeKT^]lSƍ1j(oZu=HOOAR$sQ֭[ 8ݲWBףA%|]TPjڪ֭[^:j֬Y4=z4ԯ_L Kc|ƍøq6(i񥤤]vTt&MFXv-PȇM0 /_Fdd$Ucǖ*x("P(0l0>h4b̙X"6m-[QzuTV ڵ+RΟ?֭[Fh߾=jժlٲӹsgzj( ̞=-[D`` 6l+o߾ػw/FLL  /5ƍ [|1\r۷oGLLL&!//#GÇc^;nf4ݥݻŁpΝ~=11=8`e`00V222؝;wXbb";}4۷o+K)))ߟI$6x`|ro0J$ ۻw/cdwǎ2T4h"""Jn\\kf[oacXǎYtt4{wD"a-[|ʔ) b'Nd3gd/TdYfXPPe'Of?#/YZ{nck֬aC aXYdd$g16i$k׎Z}wlȑLR1Bc]zEFFjժ1lҤIlٲe~mܸ۷:u%&&;w'O\f0ʜ6!.gϞext$.cرcF3` =iZ>ٳJbӧO|_0///x U^Ǝ$c/##VWf؈#co m.8t,ȑ#f`7o([N!|fΜ)߾};{T*StK+|kfjG6lc%%%1TjժܹjSN ·qFEFF ¯^$ q VT2^/?~<m۶e؃J쥅}ILL °0eՎ;\.guԉ͜9uܙ1cqg̘S oԨQ b;vd1Y֭VreM82ח%%%1&|zk׎`/={6/xV͛ؓĆ֭6mڰÇfg>ѻ:=z~ * g\0` "88.]<ݢx)rssqAAW_}FL>UVj,\ v ˗͞0z_~%n \8ۢE Ǐ-Òg'wt*U… Xv-kܽ{CVV/_^g8}4N8=zs۷#22_ 6_cÆ hӦMm'Onz*chРY,JuV:t]-Ç#**  B.]гgOt /"*TKHHLiٲ٤9s`݂#Gb۷/ΝϣF8p tnݺ~%ڬP({q)ʕ+Gճ}y&0k,֭[[n?qa%7}t7o/DʕpLSb׮]8tPڵk *'NDʕQN# ߍ75*J*%ݢ.]cALL I 9SP*ܹs={q ,[ ۶mʕ+rJd2QQQ[sDzexGdhذ!wn&aߜ-8EWZ0Y|)S ::ŋGFF l{5m4zzfܿ1116mjժ={G=jĉ0`.\(\t Z |Ӓe >;Ap(J~^wE̥+ ΅H$d/RJ{yy!$$d2d2KM6^zذapI; DӦMd# ɓfl4lm۶k\,&))I͛Xl?]6F]vaŊ}5K7!!lorPJ["** j]tŋqeԩS7n@bbb;vh4bѢEѰaC( V&+ͳ)0ǍkK? &'ܿw}7oĚ5k0`#!!/F i&L4uE֭jqիW /`tCCChVǻw`P6&""fDwܲo>L2r~AR:}:d|76me;v4;vyyy1~~Zpkk-˪3@16l0~5m}vt:jYd>M6 [r  ou|se{gvﴴ4T*7/=>fϞm3[>3,))߿%''۬+Mwaڵk%Ƕo6lbcc^r/… Stâَ;إKʮ]&_JJ ۹s';}Bv]AXNNKJJb?-Ͻҥ _|^}UAܶo6{ѣG3Th4ll޽fc,44ԬFT qiIKKc!!!|3bŊ8pqy(Jԯ_R}-[80ШQ#~+bKΝܹs~:233Qvm4mժUʞJ*!)) )))`!88آgv4$|Aw>`Zƍ]g *Vݻ[:t= IDAT8mJvڡ]v6G&YpYA7pH;#\>  LawHg Cc|3AGݡLawHg CG8e>-=x GQ~D9Ge{`jJH$ ʊT*L}|˵Ge{`5)Ws$\BJJZ d2L @koǣN:Zئ+JwnFZ BB\nևO62 Z9991t^V?@f:*ŽP(h+JN`h.͛7аaCMDZZVZi͎=[{>g/Μ9X |3gu9ZF^^?uY&AT"//hs233P9Ӯ&9T*ܦc1デ(@י˪gϞa۶m, }GV i-JT`0`0\޼3yޱc|y"[xL9Vh4F Z>g-._իW###Wz_gQU[Lٴv՞{*Gb̘1Ӛ7;\>WSTP(Al?GXV lڴ ?=5kJ*微cP(5T R ^/p$SE^饗Xs < } =yܹsE^[~=^u[$ʵt hOED:(.^QFʕ+%m۶ǍQ]A@p] A+7o=֯_DF%K`РAغu+-q0k L׮] `}+"E7FBBٗ8nݚ[|9&M$U@]Ϗ_nPpu<ŗ#FסC* w -Ax4ƷpB4 ֯_OݙNZ|)-K.a#,,LD9>SwFjn`#9۴iɓ'lQ>CGx*de/3g* 6l.N'~CGx2<ΗygϞ hQ$|duh4bȑhVA`L _TTN< P(ذa|9j; իW1k,̙3Ѹqc-"J:>“q1>F@&M)UDI9jVX#Gr96l@$|dIRRR0}tiмys-",; ߿/6l(#>h^"p2Ʒfp,}6N2e Zn-EDi!ZGx* |cƌmz 8᩸EGGRP"[E>|c|CDDyҤIh׮eOGx*{ G$|"@Gx**|?#vxKdB'$|—?Sq1ѣGZj?"ZHDT\?cu pMHD+ _zz:&N=z4u&E j+ ߿oW%Kla+HDZTZPt: -*~ ۶m?w|K6$|"@->“qVߓ'O0n8{g^zhakHDd]>ܻwPJDEElakHDdY݋M6W^ -" رcׯOHOY[|SLAjj* 00_O g|ƘcqF?~z~-" d2h4:M'Gl—ѣG AhaoHDM:7ooZݾI)D$Ђi"J ʽ4+rA\@^fE@&H ҷ3̖dffy⯤N|tբHDGѣN/]ѢHDhWR?~< z&MAO"#J+geXfhxJ|Gx/^Q~7<$B+`a4={x:?$B+8u 00}d2Y^(IWb~7:7o^x>J|y|_jT*ݐzV~ha0xjwBO"#LVo_~jvZt#$C?|YYYXhEЪU+(ID=:ֺ{D3?~ $Dڸg61qDڷoٳgķ(.?GAII nݺO?ǎ㷗GhhcX|9}8pRvZ(t#(*$Ds?D޽*>|| ~XÇŋ1g~9sp'Q?HMMEBBN>]}j \>/MV}^CY|_PTbݺuT!BHL& QTTܾ}_#77bo-[JXjBcԨQ?jkѢڶm[vш*7mQQQNcI]6mڠz͛4^z K,vٳg㭷ުr{ff&=FXX/_ѣG;UNS$ׯ_bUnW*4h%"D\FBXXkÆ sتL& /CEn+']$3T`Ϟ=mN @L85Gݜ{رc,YBIQ@tt4Faw$g[xHMMvR+_k$DfΜiC\B!C ) GdP%K`ǎCKI$)) ]vÇ:vh KE< ?ҳ>m۶aذa0=nyظq#:vƘW]d2YQЌ3,usD㓁5ޮ[nܹ3z^_O<˗#88RL&Jҡ:%d4ѬY3\zpnZRpl6_s}-{4i~;v@ZZ%r.d2rkab$J%}Y̞=qqqu3L XčJJJ J m->{+RPP&MgvUzb'aKl6h4d2nݺI7xyyyPTP(P*-@o]or:qo^v_/nAAA5j~ao)]H^6\30 0 RYEEf3?PTRғK;3s!** :Nx4U; @A>ںM N|֣JQQJ% cPwh4a4-N7_+ BzrKx/ADL[ Z|܉[p\kB"7+--B) fz\J|3F]"t:ߕέ]I{z4L=>oλ~AxDƶJ,jitNo~zBܾzr#N]$Rrfw"-1<[&&-2,@w_/bewaH׼=EI_kJmR0j֭قf??݂R.U['>4| cZ֨թFf KM3+ٮY7ڤa̷P',f9~oxf˧3߻`J ţۈc!B _5GMPCx|{ ϱ'0s^87EW9,j;w^T,zcw yW9a؛4i'3 ^đ>F֡xCh2&σR{ш/3at2Px+g~}[O 4a9Oڊlu +GF]n\_}K-Rs4t%nve ^AxCK(`Pkv40k!6 dn߈^A,_kg;(+*q؄Nh1m2<{nyy9j5j5 J%)W;oϚf\b$tF>լh߮?z]y}RGLܸ]s".x*[7ru":NVwlNcʈo?//sBCRC#***.$ִ*Vٹ.s{ܸkMJTj݈@ak~r8k Zw+uq$ۍ{|D\. n o{-ӕ FGΉ:6Ag;k^CٴN-e*re>T{b:?񖓊0q)|B{NAx0y{O5^pu7Ly܇1S{a%Ё/V# rc?ѾHU{xK,Y&L~Į-~ 'g]v'1$ WsgoGfnjwL&S6F⿋⿋" ]CLB' 1 7QCHF¿\:X ?ʛP˿~|)QCk`f3B"BnEyBBe6n}{t5AUǯQR{ .B{䲻InD$ p7܋.O`uCeId2֏B1cW]: QiE}Gu?ޭNm{ -ڧ̽: Wb+@G0i5hkQ4} h ܾo?@3 )զ^1O,^3*cckxpDzb{ /U; kݾSZܵ{oZmEhޖlM#k`uGt6A'?2bp 4l;.Ɛ^{k4u{=a*5!>ʑ_\ܵǿmP[;S/t2#b8+tpC7Omvo [|Si"|ǩm#a ($go߆.@k0+PI !BѪs*ur x1BӃ[JMPQR]gΙ}sxf&C_^n?lT$&Uy}K.'262@^Y'lSR[5*wdVC?8WjZx!2r0V.ٸ}_k(_uoFڔUf@W6vnp.?ej_L|-ڧ`׺%:ܡwB @ܑ=P(UMlwo?~WMY'>ܸsG @m2Y\am F]7ažYȵsFJ:coşZto?LT?~js؎ \>נOVy۾1bvmPD6s 6Z6WO`җSS*@)T*MK_ES2)(Z\8[mUx0?+ٿ^q♂L.GAy IDAT_߼*n7Ӧ#T-f}_?nxЁRÁ`@Lק[/X'wmAlUڬ ?N=l6CCӡ(**B^^< ܒerxKf=G7b5 8fZ%T-4oC0`#r?KpӭkOܑ=7oux}Wgû(/*ȕJ~'a{Kd2DD7CLbZV[.=V DFF"<<!!!F`` RPD6_Үqy0~UKPv'x (o@ϳPWg 4Z4{kzb[M&tzd2͝"NNi)6² ?{pSi'L@"ᚱuc#OKu^OuqEuLzr"tu*\!ܓB<}:Fm5|)=YP3m=E;zʵy2z/akϺۓ˩'N準jB,Lt\J~sy|]B#rGTnq<}!HE>.QA.BnуhRQJR3$wS\N%PB%;B97:4U!X3 ) Z|y{sT!X.G>IA\\F>}qƘt9!9d2rVXXrssYvv6;z(۱csTTT-\bۑ#Gݼy1ܹsK/-^߯O>,77Ϙ1`/6hЀ`/__۷/֮]ko^^ baaaL3t,**`{eZ`III5edd;v#Gl XEE3LRѯPW'q?/_F\\,X`-)) =`߾}M6QFr-^'| a]vݵгgO;]BHN6'OtVv \}Ahhh*]vŋ`ܼy?8ڵkBaÆY%'x F @#nõndP(5zpgm*>RRRPVV%KSNWF?0 6剉{&M8]BHG܆_h4"//P߿?f͚|EEEؼy3F.]PTT9B< %>6͚5:tDnn?,Zoֺw_8qnݺ>AAA8~8V\ h޼9L\za9!GܦM6*Y3L8p zwիv!wP(''7n0a^xS- >>7-~[ZBFMzz:ԩ'.lذ.\@XXE9~ǎT>B8֭[9s7nǏcx;wSLFΝ;wY웝|BBHG&22 . <SL%KЭ[7?b ( &'%%!$$999ڵ+fϞQFcZhaq|Z3g1.]`믱rJB!~'\•B?ws4\N|r2 bK.[|ϙtK]֭1$gHd'??OxýN!.R Ba|)U\.BJ–-[B0 0ʹ5!p^ SZ'?a\jRJFFVdc O|1J|ĝ j5jRRT*mqJB@@a6+ FN쮲BGdPB"00AAAj|O'>K|ZzAAA*~j5z=F#F#%>BzF'?".ŧVa2`2OAAA|{_&%?V! ^㓆K[W7– Hz=?BE^0Z|*b %?qJrj %GJ~_%1azĸ 7keTRYy%T*a0dGI/O.;a£nNiɘ 1߷30-<.qS_Eɏ⋬8 'sZ'SKL~1> "7^^y=KzS<0qݘkt 1Pғ]lfWR@{ ruтuzs_s^z]N{:RғKOp\ ruϓpX.98`ٓ[#u O\,B_/dgJ~p: \3L t[r;g:u$.|^EEEʝ(l6Sz0_urO'>[|,uOz)L^wKxzJMr5`- Pz_8%q) [zܜ=_P^^Ώ95t|7#@qm|^)%?q9h2`4a0Z8]gGp/+ F8\/{qJ]=>ۈ;TTTl)--'Ƽ֫fdBq]|^U)">w.//wg$S\\ Vg0 R-|^%%%DL&+I8;jSKok]Heee`A+o<_Wyy9j5>RXTԻjS".n|/ zUTT Ff5EqJq qJg}s!%%׮]|9sf3.]}a(..5LΜ9ɓ'C&aʕj*L2Vȑ#*Z]G}?5Ž;sa֬Yo1i$={Vz+SSoSǯڪU+~Wm]vE1a;wN9z߿0f[G_^\U駟K.*jժ֭[FQQuSS=gY׋1[B&!::fݻwC㡇Bxxeu'ӻ(N'g2p1|صkd2K],9עEk׮aڵ޽{zYEudh߾žٸq233CR駟N8EqJSmg_'ro4WŞ~R0ũzU-}*hOR7_WMZk-Xotc\pW/=ש^'ũzq)SODbc>wFFurMvO"Lv)LqJTʙC}D kqSOPU%{ Gn O4Dj┈ǥOh9] &)┈ǩg}LsQo=z?BRU9⣓++kBT]q9Ci f⾉p+S)\j CYw+"&.- EqJ{נ".M )JuqJrޜ"8Wɠ8%R.N)&EB %>B!~!BB_G!įP#W(Ilǎ5k{Rc1L&ADqzMSb%>8pk׮ٳg>l2>|؍%]cǎRą . / @DD4h@$&&bRxJ|.ĉk."ׯc۶mR?#GɄDFF;v,L"uG2͸x"֭[޽{X"b̙3,ڶm_~?7nZl ܹ3ƍf͚n݊^zڵkٳ'͛^zh4?meL05kƍuØ1cl?>F|5 sAqa͚5v^YYYxǐƍfY~-ڍX7z4jԨ?`2Xyy9+,,d,;;=zر9yHݾ}rT*Yqq1?0Z͂^-\`K._ׯ6nhq ewĉ|rcbbpB90͛1Ν˿pBf6: O8r9 gO[ne*j_/^=}avF8ϟwedd;v#Gl XYY+--88z* c{Q]VTT0$I׷"""ww}Çݻw{qqq=z4yk4 >p5qeaےCjժ͛gqk׮Fii)rrrٌ̙^{ mڴ8ΰaпzlܸѦ|شi]T8D=PPP$L:ե{bޤwɓ'ѳgOnRhڴ)~֯_V ())3ga5xfɓ';v89pv ш#FXޡC( [h'n.UB}r`]j3N 0m4tRSS7-;?>'q`IDATdBjj*k0 xmoaԩ_>#??evd2( M&MTVݾ}xh4ךPWF˖-#44V®]p}9}L[~:o8z(***+T4i-ZC`0gGOO?$$$`Сhٲ%v͛7cvq'S4Msc=Vl vQ8-(( m6(J̜9GXX[O|O$>L??}~}z+WDff&vލ<_}>SDDD`h۶݃Jfp1:t&L~y\v qqqBTh4bԩ֭=HL& >{EBB222СCZ{DW'po/^zW\7Җ-[pIݛGP0d P9 ٽ$fナdѻwo{!BTW~}y$''8˝qzjݻ8p%=R-I|BU&Td2l>V8pl2ovJ{+Srrrl&nذ.\@XX+wodӦM(**ؖyȑ#HJJrwAqZqsN+j o>BCC̯p!™4]7`XFl } '`-Z`{edXuk޼9p Xpp񢢢XFF&vթS'm߾R6e`͚5c|DbN 1cFCGh(NgϞ9~Uݻwqݩ8 1VVf3z=t:JKKQ\\"!--F=$''LDddŐnɄK.1MZ^3L8{,.]Xj jڭeq~ܼyHMM延L&CFF"###001 SSIUv('jY/M!įP#W(B+!J|B %>B!~!BB_G!įP#W(B+!J|BӉO&ً?5S)S".\.L&?\."S"bR\.uuZ_M nIXIxGqJģt凹*{(J( +iHQ8Zr9 T*lق|%%%(//^`l'_{µ 4 PN#$$j?T(NIms6N)ϥdT*Rhhja2BO(1:{ŘZO*jPTPTP*6N8D )KdR@f H@d2l6ďd2>ƴZ-Z-V_M e2cD4)ӉO}ĝPZ-z=T^jzFF:)wB FO*UusRlqWj& & LPPB&Y'IE" AAA"wBqJJ4Exu#B殠-N&ܽBwh4PT|BQ18c *&ˡVa0,N&IN*^q'IkqWJ8%r6N2S< T*0 IN&'\_5 O$UuQD2;110W܉n~TȽ;* ̝T(NX\S"p}͝DׄhnqVuo[dBqJjqW‘qt!b {&D,)}nI|BG'n>n┈ܞ!OFmB!~!BB_G!įP#W(B+!J|B %>B!~!NIENDB`circus-0.12.1/docs/source/design/circus-security.png000066400000000000000000001442371256046442300224500ustar00rootroot00000000000000PNG  IHDR?d@ pHYs   IDATxyX?0 0*Yjie{}OfYVs*j生괚-[n` *.l 0}tf{f^g^n Z^; """"gbCDDDy?DDDQXGaCDDDy?DDDQXGaCDDDy?DDDQTrRgR(M-7:ً+:ŏx@ A-ѵSJTJyy9aN w3%.[xL&?-'˃JP  fYN"N"g3.YX4c2je,BCCq_+H߆NCLL )SPCpp02224/gφZ5k֬9ߒ.((1cছnBxx8>#;Vw^@hh(&9(pkdeeAVc̙ :|0j5ϟ/]g6 ڵCLL  V)S4HiN^6bL6 NŋR9 V,ZH^F={SN`QЦM3<_QQQxgѾ}{cݺu8p ЫW/:uBVV HIIh~pЎ;УGtϟǤIP]]˗cРA(..;#yAaDݻcƌɓqyxqqd2h4bŊлwo 4/:9c=z =zƹ}_p뭷b׮]BUU.]$r+-okI~>}Dyy9nV1_p#,, x'駟K,AnS]oMo5'G&N 68W^+QFa@JJ >3L8Mo׮]~@EE.]T*]nj!\p֮]+Ǝ+]w} 6H͝;Wt0w\{ ˗ ֭> B7oto-,YbmYYЯ_?p1_ x{{ ;vhk*·~+ GN>-\xQF]]sرcJ:t Zֿ) ̛7O tηqg̘!8 iӦ \AZwy@fw}'&O,]/JKKKLLlEtuWQAh}9ra0uNx"֯_ooo3fn&e\|2jjjξ[-<Z Z/@^vVZZj5۫Xn`رAhhM߾}͊k%&&6xBX"77Æ CRR4T塸cƌ3go 77{ۡj1j(ԩS/-܂1c 99QQQ^p w} .npܹsktM n1cf̘qǰx\[}yyy᫯'W.rdu]x뭷0{l3&LC`ʕ短h",Yn牨_~ 'Nl] [~ooO ip'Onqo3@8V>|R$W[jjj{n$''KS;wݻwCMM {=̙3ڵÔ)SpqKB,\EFPYY ^[HI$}ݏa+̺in~I~_SG(ć~!C`ǎxꩧ0p@㭷ޒ+((.ךQϞ=l+#*//o9]'iN,Հ'd2!==)))h߾=뇑#GB %%騩D{Ƈ~رO?4v_~Ǐd}DDD˷~׆܇ n)|\[WsQE9P(p}a߾}8ucԩ8<{1|5k4x}駛|<l믿@X8ݻ˫1bbQ_VV>>3f ^{5 ""N±cн{wj>>hVT>}:AC=UA;?j8{u -y\q,Ǟ={_np47{ꅸ8zlٲj>G!.WWW}%rT$Gz __[\]dz>9&((Y1<7|fYH)<ɓ \WW\\pAd͚5 عs'&Oŋ#88v+N_M4 .X?IIIxQ\\>/g}W^ALL Ν;o999HNN|Mlڴ <ĥKGa۶m1cnz=L8Ǐoɓ'c(,,?> #G @xx8RSS7ߠwV߬[⤄ygAϞ=cÆ tV 111ؽ{7-[3f`79*GJ°aow܁s믿F6mfuIIIؾ};nyhӦ vލ_^^^V "6V^^1c ̞=J233h"7^?a7ֈ:?/BLLpwﷺϵ֠j-K@ܹAiii J[^&N([m111ViZaŊBeeնz5^A,z^XhP(h4£>*H5Oc9A׿%ji6m}du~e*tj ;w.--MQQQ?W\wko57GXm,dee5X̙3ѣhpp7{m D\:vh̙3q; @?0L0 FUU._2?gφΝÇa4nݺ5yuEdgg@Zo߾658v9VV1KPoEHHj FcժwZCWTTСCFTT4Y[WW#Gh4"::A 8~8 н{w^^ G z={!!!puWkGhss:@://]tADDD'NݺuCll,[[Syٜϓ8dff'N_~ tc"@XXbE񁿿 AP@Hdjt eee۷ܡ8enjZdPP0G`;V>}g$%%ȑkۊѥKrf\4l*_Q*Amm-`4a6-EGGr4b~T*h4 ZV!~pdsy?@[pB\g^7sNDEE!$$DP\3% ??XT555VMk?p%>>?СCCqJoVy^uիΟ?Q]_r:ٳgq-|uK:Hj5@xyyARG:hL&\ښL]v0 0 ԩ8,v- sbmu>|80~xCq9rymňfmW/X

4)-q\p$ !AȐIІ ހJ&- `ErP@$#`xllDDR!L V"D]PM|v;ged'FpUHH@Eġ jMb  UHEPCjt Xȹc# aE|ʇC"LSYhOS@("pY8 9F0 qX"E&e2tBAlAOh=95]*(tv+yU(0pei8 @b-xH tȁX2٭1rfI[QxNr^@% !!(-=Nو ))\r@5?IEXa0 RI8-xڱ!LQv^jp3Hi ~X])* [CR' 1Q2&}D X TBDzh 0) /Ԣ `kt6:^*EG`\tqM)V 5ܠ }()0l1\!]E1ʁ==I>O\0[k  Zh@ά ?SjgЂƀ#qHA*;PgDh #xk)c5E$m2bb $9 !VRo,6@R=0n%;Cr$ψ5L_؇)}reXWJVhpa@ ̂@D xЌ=L<yb2&0jZI\\ah̃ĂAlyc`=|pQ8" az0r@{ w=ٖCWƀN;VXDqW[@'MR 3>) \0g6*qPD eVf2$,I`۵ @A{V| h$bwPLHP-(k'4t(Z-2bUb7E!.&eL9Z#9iaK& 7sz $b2*PHA EO#[,풰v"6l 5<Z=190 gvU)0$AGPj#W n@Q;r..R,Bv?.bSF5 0D 0:E_ H}0c O$0NV2 `n \F ubğ,c|+&~ @2gk(2)dk!b@ !wcoSU;@C;Pv8%qэ ^7kl@K{r"( C"Bݳ fna~O@ 40ߛ N4+I%7JQ<3`D,@ |w&+8V*(ơlA_%trgX@:`qMcJG(8[ K ;_P&In,AAv/G)Lt=#mPàa k#a/w/AqI j@+H@VDZ d I0ؓ]Lt{2 T Gct+UX ${܈#KӴ̼@i<&)rLT* &P$'Oap{ =pvLy9p͌q"Xd*y|k`#ՏD0NxɟvlA1 (`Z(H* C='QBhMN`A όf̸{c|}fV9@٢ Ǐ\.-!ѓ߅{`5^P (#*rHHAXf U{ٝA =D2*GKvj!K XiHnXEdT,2" pb+Z}Pr꽥P#{ך/sMV1 ᗀ 򄜒"YeZ Zpi%4ǒcH"g*[`M`f թ2@z@'ac"0MrEdsK;Bt(혱~ h(9<֐:ׇװoK |!"ܛv/keDHn`ћ`."m*+Z km׍ .(Y"@ .+GIHF@&kKJ %FFFGŒG:8XԤ`9üFHն1ܕI9pe¯6 XSȰy$[_*GahC2ITdFTU'HdzPQrH\aYOz|@6t1b "OSuի *o 9v$KjMʪ]˶0K)*%a_V:et$WOI,+I #̹s91udӨe%FMZ1>SR-DpE~+_>䦖+A4Flڒ&+`2({hF} 2}pK'`_-cɀu.Qk@1Bh!W/$vTG$%Qxh"I `@vq e+v8CC'x9IH" -]%bP’T>QA\"$G8ii]I5N1ff"vlX$y杇@IG矄D)8衃} wT0;;circus-0.12.1/examples/webclient/templates/000077500000000000000000000000001256046442300206615ustar00rootroot00000000000000circus-0.12.1/examples/webclient/templates/index.html000066400000000000000000000021261256046442300226570ustar00rootroot00000000000000 Circus Web Client

Circus



    
    
  

circus-0.12.1/extras/000077500000000000000000000000001256046442300143775ustar00rootroot00000000000000circus-0.12.1/extras/circusctl_bash_completion000066400000000000000000000021411256046442300215410ustar00rootroot00000000000000# #########################################################################
# This bash script adds tab-completion feature to circusctl
#
# Testing it out without installing
# =================================
#
# To test out the completion without "installing" this, just run this file
# directly, like so:
#
#     source ~/path/to/circusctl_bash_completion
#
# After you do that, tab completion will immediately be made available in your
# current Bash shell. But it won't be available next time you log in.
#
# Installing
# ==========
#
# To install this, point to this file from your .bash_profile, like so:
#
#     source ~/path/to/circusctl_bash_completion
#
# Do the same in your .bashrc if .bashrc doesn't invoke .bash_profile.
#
# Settings will take effect the next time you log in.
#
# Uninstalling
# ============
#
# To uninstall, just remove the line from your .bash_profile and .bashrc.

_circusctl_completion()
{
    COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \
                   COMP_CWORD=$COMP_CWORD \
                   AUTO_COMPLETE=1 $1 ) )
}
complete -F _circusctl_completion -o default circusctl
circus-0.12.1/setup.py000066400000000000000000000030061256046442300146020ustar00rootroot00000000000000import sys
from setuptools import setup, find_packages
from circus import __version__

if not hasattr(sys, 'version_info') or sys.version_info < (2, 6, 0, 'final'):
    raise SystemExit("Circus requires Python 2.6 or higher.")


install_requires = ['iowait', 'psutil', 'pyzmq>=13.1.0', 'tornado>=3.0']

try:
    import argparse     # NOQA
except ImportError:
    install_requires.append('argparse')

with open("README.rst") as f:
    README = f.read()


setup(name='circus',
      version=__version__,
      packages=find_packages(exclude=["docs"]),
      description=("Circus is a program that will let you run and watch "
                   " multiple processes and sockets."),
      long_description=README,
      author="Mozilla Foundation & contributors",
      author_email="services-dev@lists.mozila.org",
      include_package_data=True,
      zip_safe=False,
      classifiers=[
          "Programming Language :: Python",
          "Programming Language :: Python :: 2.6",
          "Programming Language :: Python :: 2.7",
          "Programming Language :: Python :: 3.2",
          "Programming Language :: Python :: 3.3",
          "License :: OSI Approved :: Apache Software License"
      ],
      install_requires=install_requires,
      test_suite='circus.tests',
      entry_points="""
      [console_scripts]
      circusd = circus.circusd:main
      circusd-stats = circus.stats:main
      circusctl = circus.circusctl:main
      circus-top = circus.stats.client:main
      circus-plugin = circus.plugins:main
      """)
circus-0.12.1/tox.ini000066400000000000000000000026071256046442300144110ustar00rootroot00000000000000[tox]
envlist = py27,flake8,docs,py26,py26-no-gevent,py27-no-gevent,py33,py34,circus-web


[testenv:py32]
commands =
    python -Wdefault -c 'import nose; nose.main()' -x circus/tests


[testenv:py33]
commands =
    python -Wdefault -c 'import nose; nose.main()' -x circus/tests


[testenv:py34]
commands =
    python -Wdefault -c 'import nose; nose.main()' -x circus/tests


[testenv:py26]
deps =
    {[testenv]deps}
    gevent
    unittest2==0.5.1

commands =
    nosetests -x circus/tests


[testenv:py26-no-gevent]
deps =
    {[testenv]deps}
    unittest2==0.5.1

commands =
    nosetests -x circus/tests


[testenv:py27-no-gevent]
deps =
    {[testenv]deps}


[testenv:py27]
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH

deps =
    {[testenv]deps}
    nose-cov==1.6
    coverage==3.7
    randomize==0.9
    gevent
    coveralls

commands =
    nosetests --randomize -x --with-coverage --cover-package=circus circus/tests
    coverage combine
    coverage html
    coveralls

[testenv]
deps =
    nose==1.3.3
    mock==1.0.1
    PyYAML==3.10
    six==1.6.1
    papa==1.0.5

setenv =
    TESTING=1
    PYTHONHASHSEED=random

commands =
    nosetests -x circus/tests


[testenv:docs]
whitelist_externals = make
deps =
    sphinx
    mozilla-sphinx-theme
commands = make -C docs html


[testenv:flake8]
deps = flake8==2.1.0
commands = flake8 circus


[testenv:circus-web]
deps =
    {[testenv]deps}
    circus-web

bbbDM8sݹ&LK,;sf32 8t'w8nőа LXPEرcܡ`g⇚< Fq,~Iqqq8q.^(w(D|:{,z="##5IR!!!]j8qBPp{aCĮ/$ǽkbC]q/,~뇪* Í7)))0 rBnNBllܡP3faym"22{;riiiR;j&?,,~ȓZ]^5Krr2oA8ZŏaC`dgg >\ߊD||ܡP fcy JcDz)##;j?l,~ȓ닚'?lcƌAZZG0a6o,w8=fkӦ )""NG IDAT"#G (ٌt!?""O®/jʑ#GЩS'kNPXP!O~)r_,~EFl\rEPnرسgzܡ bXPx{{#>>)))rBpΝ;\Ν; wtXP< ȖfDDD ]?b,~ȓ![{cC-b 48wܡ xZ cǎeyBqq;YaXuayv}/ٳ0`ܡubCׅyc֭0rB. ==GKϞ=T*+w(DשS'brB.]^]7'a8< ^l :TP떔L&C!rQF!++ rB2ڷon& ?tڷo.]py1۷o;,~$%%À<On'曹ء3ؽ{7VݐDddd@ uuu8~ܡ m ?tC1p@ڵKP]_]^aC]_,~qyq!%%APX,~ :xܡ9\pp0tC!':qJ%u&w(d,~膩T*$$$`ǎrBy8ާuaCvq?I8i]X]pyaÆĉ8ܡpO"::(((;"ST;v,[<ą P\\ 'aחHKKŏւIiApL07o; rvy>,~n}vT*>>|}}}||/eU*1o,hXf |~ rm{Ell,URR;Veakl6(**BQQ QTT\xQ*n" jt~f(JXQ(򂗗J^^^r Q]]-.-hvI`k!!!V?۷o`,cgD)Ǝo/0vyN,~ȮƎ `0@V۪_oժbwii)rssgϢ%%%(..Fqq1A@hh(:wpcذaرUajmԔz௿_ϣN¾}PZZ .󨨨@pk׮ڵ+zp"˟.@-Dv#<"wdg S rVjkBNN?ǏCTwׯt"aaa E``,HF8u PPPSN8z(ѳgOzBTTbbba2eY j9_ (--o-ώØL&ܹshӦ}"{* /ܡ4[;f\!++ wƁpiAaDTTڵk'^J;v šCpa_~Fll, -Fqwѣ2!ہpBrB,~oߎ+V׍SxL&}Fj?YeSoZ5iP(?~III8sܡ}"C$%%ᮻ; YX.2XtyyycΜ9ؼy3Ft:,X  >-[F~˖-ZYudk-r׻zEc[Ja}Z7Cfk999ر8Mq,O]]ʰzj㏸{裏0L駟#$$=F Z JJe5P%lE7"wU/`krTUU555F(Ji?ċo B`` T*U+l7Ф,:ˢE0d[/_Fyy9@m:t@ΝKhh(BCCkҵVn%QҰ|r=>ɋ-?0|݋uCY>3p%,].]ᅬX%gHLLDff&=:@VCV[-hKc \_l6YYY?b̙3(..FΝѭ[7ݻwG۶mN "^{EE˥ŋqY={gΜsPXXBGҢ={Ddd$"""^'[p_k_!22.]rn8`9RRR^y<#j/|ouuu(,,̙3_~x___ddd;ƌ0Rw;b5ޓؒsQdggȑ#RX 4@ѵkWT{OҜΟ?ѣX~=rssQXXnݺ馛0` 00Ʌ'Cvȑ#Ybl!ڵ+lق޽{ךҜm-bybkhDmm-N>㭷£>ڬusaƍ j#1ͨ{bܹ3 X 0 hpqdff"33Ǒ#Gn >|8bbblheСyG&g%8՝m8p .]6X^zAmo6uki[3U,6->EEE1cnv͎Zxgh` BjٳgҰw^|||Ν;#,, aaa߿?BCC`!TUU+W RUTTl  1ٌ4>ɓtRvmx1qDhZj50o<̛7& 駟l2\|ӧOٳ1h XܹF{wC7vkyꩧ& yyy@ff&<|t}Att4틾}_~n`2pe|l$g}CP`̙={6z-$AǏc=ɓ'Ź٨V^t=l Dx饗0ydiAJ///rk+l_x,W16L(,,_o՘3gL#Fp\ш7bժUC=9sחEP,~ҵZyēnܸ| 0k,,\ 2FNDd_[n/'Obɒ%?>X`C@E噛ϝ;k/СCq]waڴi1r""ڷoV^L<쳘;w.44srEAna%yEVA1 Amm-ߏ{WZ_q뭷!VoСw?Gbb"6oތZ`0h4Z}q$ǖU-=Xz5 裏s 'xXz5# fW[X xsfYb0w\\秕?ʕ+Xj~7lذ9Z"֯&L@Ν;jjT+nV2}jBPPy6m***ji;@W/|,gs[nAtt4~W>DDN+M}g.07f1S0c L4 >Y'"FCFMMUd-9\Z\t ǏO<{N<^@@RRR0x`aΜ9P(V]^L0r<)Y]Oii)ŋ!"r!ڵW_}_~m"97djU[[˗/Ɯ9so&3l0,\?4G)4Vz,[ ]v{'wDDԈW_}ΝÆ &cŏ<(,CYYrp3 SxױvZzCr?nFGsa]_~%ɓ'CرCc4yx'a&lX|9֬Yݻ&5?uIc,[~X97` uuukÇc2GIDD-1gdgg uuuV?r,&, +???;&wxDDBƾ}0qDx{{CѰq]Jz=|M,_:t;L""cǎ/wv}97aY"##“O>)whDDt 3gXX8l6K jjj'`ժUT*]l Oc~\XSXX lذAFh4u~lqqb/@oqmAmbȱāΖ'/Ws޸q#~7C$"k.tͪ;[~\XX퓟ٌh#"{nt֭yX,~\ L>}a̘1rFDD7`0СC֭ŏ8'++ rGDD7wE=P(RbȱX ˅ )bѣG1zhC$"`4GRR ŏ 5槴:u;4""9JJ///a cqQ\.\___.lHDO,[ f͂4T-@ u>/шtE:UWW#::IIINfV"HTX0q?+1L0a"##1tPnqX^fJrFDD-d00}tn___x{{CVK9WxvA F^/cdDDRf'Nٳgq-@ߪlqa+~f+snCQQf̘6m@" Z~~~ Z~X8&9O___رcrEDDͰaDGGٳ2e t:t ?cX0[@׮]qA!"0 {xb$''#!! V+u{YNwgsqAI/.v%:tCdqF iii={6z6mHŏX-?*X,~\ ^O?""ra{E߾}1aM6V@VgY(JC;4""WZZ; P*9s&';AAA GVc(JisJn ֭;L""ui,^=z[nFppT7(~V Zϋŏ ,~J%j5j5a;wN A<\EVh4h۶-{ _ܡ1L?>y#,,])))ٳ'^z%gy?˗/ǐ!CLP=zĿ).rACr.?.Ȳ˲h4ƌ7xBll!_~^}ۇ/C[񓟟h4,X.l7nDAAfϞ5 3gΠC BXXz  ŢG\G\YVsJX0[%6z{{CaĈ Z-w~lܸӦMsٳ{/t:?Yv)~9^^^R+/aڴiذa+%7`( 9s+V;"P(0d<سgJJJSO III?>;TVVELX(˕cʔ)w^&73j(w}5kXxꩧ}bѢEXn˭;}4x|'9z?~n_}t _|,[ #GDPP틹sb6'77<|IsEϞ=O?]믿Ck5ӧ㣏>BII .\#::˗/GNNjkkBh4ZA`,, A Bqqpqa֭[{'.hZaǎrM.`0G}$ :uqqqdjpE C6l6 RǵDFF ǏȰɓB``@Xre犏aÆ իW[]W_ > Jl߾] tAɱAnݺ %%%B߾}BDDp&#I _|QhӦpm BYYPQQ!TWW `F`65zA2r܏c9@ӡwޘ8q"M֬oD@_dddo_|E<0Lxᇑ?| 틼< 6 ǎSO=֭[uVt7|3P]]KX^QQ!>i$۷cܹr f̘Tc߾}xP(駟瞳5kz=&N'x}i(..ѣ^z!55]tiI]׮]b  22xᇑZ`0.ibOMMpeD8yp!!55Uذa ob a{;trQ[~DSN:N(,,Oc-?gΜ|}}|JKKHtR>S,}'Wߺu@}O?$BCCf+O.1[K̔[~BZZZo9wQQQBQQQY*++}Ym۶o!?^|PYY)A0Ll>5,o/]vŋ?bԩ{Mj#<Ҭ^z Yj߾=_|k> P(m{,hSN_lAAAXz5ۇcܹϗZF4(Z1al@/VkUt:tz+4w@xx8^y~_9p`э["vc]t 5cǎSNСCn9r ** 1116OP`reM|i$&&ĉP*ѡC&CdG@||<&N-[H`Y7`9GUG* xݶ/_ TVVڼTUUIgnMIOOdž={PWW'0X jڵkn0XDC -[Oc׮]6B1,~Dc3,bԶm[$''cĉx7+Vʕ+rn O<7ߴhZ^z;oiMtߠ,e_ , ;eSAeQQepyAEAyQↃ(,e_ZIιss{Ӧmڴz啒f9-7=ʕ+5^ƍRHaϞ=СCIu?jS^^p!]v.OiԨQļy󐘘J<X,uz>>}/ĉO3"j FD(""hӦ nV9+V@\\qO$oCRRV+~aڎ;]Mqq1233FQ>%FxDt4h~:uV5O9Ch4"((.Ν[#~߄tڵx'#waif @944Tfm۶;v,n[III޽;z-i|`%K~oF~ mۯW_ȑ#1m4z ڵkcĠk׮ׇ~'O")) :urxrrrpرǏy.ϙ[n&LήsԩS??(++rFCtDD"##@"###w}7bbb``РA?>Ξ=‚ -[÷~[e Ʉӧ }*5A@ ?~~~6lWk 2D}]t}p7ok᫯¥Kp7bHNNvw3fH{ =8s[|QXX۷W)~O[kv;v @ZQQ! l6(++CYY˥3 '>aX,f\~.]Baa!ʐ@x@fXpAڵ HHHQ`0}ܥK=nDZw^={]tAϞ= 35-š5k[o!88AAANӍFls~oDZRå"@$Mb<O ߞ={0i$| APP z=Z-4N`69ZOC$~FNF7Vb #B#FD *--fåK~zsŘ1cFDHz聳gϢP:#7=/#B< UVV:CKA0 ^NVEyy9!H@j/Æ #`…EDz=JKKJTVVBymB| /G}u/€|u|xrj̩6su_wy'x ̚5""j h" <:aW~дn8ՌMuUTTl6cݺuxꩧ7`n)w$''#66za^uøLɃ+>iӦ15sϟLJ~Ç#00ɧu:9\ǑfL-8p#GĚ5kпO7aƌݻ7t*? >ÑfH~ 1e6q<XhƎfQ=|زe F $}~j᧙l6 /n&L>$"zx"Oq!$$FFA ?_T;jÇrJO7iԩҥ ڷo_%)/NǑ:O3lo6x t:O7a̙8tF IG>#k͌XnXQիW#11 "j>3,]ǏdBppӌOX*F}_t󈈨ك?O[~^ZV*vffBߏ{DDTWǎȑ#1dmyr=fD^#n݊Çև:z(q 7 %%&IQ]ĝ~ |Μ9]zyDDTGENгgOW =!!!Ҩ?zpfD9SQQ .]vnѩS'tMׯzBCCXecCO7ciKEݏbXVjc۶m0`FݻW >bX%>!!! vrf{143?VApˈH~@JJ bbb0j(DDDHG\;#63Ou1O3"_ ?ׯǑ#G|8RSS$OPP!OaѰ~gG L&bٞn*ѡCܹ'NDpa14#~~~h4N^^Lj#i&n&O2͘9s&F=z`ĈFhhv>j`i\L(? d¤IOo߾HIIt|ƺup#00'NDDDtFv*.q 4(nji ?͈<Q@h4"11ÇǠAp1yDD^-//ӧOGVVRSSѱcGF3#6U60䪮ӌ(}Ĩ`0o߾(**B-pyh&":- /;ow v89?X(~ G6c0 X, 0f}Ν;yݎ?O>$0n8J;"o(^/rv`iF䫽'@bHgzX,QqFЪU+$$$]vRyH"1"$Fy)8ÏwP @fY A"STTRZZ ٌGJ!h޼y7nx*++Cff& |B˖-`tĵ|GIG=Ml0xyZ?(//BNqq4%nR:tl6f̘iӦ!""?"Qbʕ($%%!44T <6GQ334]Dѳ@VR-PiiH|䧸eee(++Cyy9***p9>|?~= =^c68QLUVVl6K@"'BPYYN8g… 9r$3A˗b ,\p<,jB?:@">>IIIڵ+:wN:!"" CXXnի(,,Daa!]SNĉ8|0N<\\v hDdd$H"S\ʀ#Z>#Rzl0xF5AbG#-B(**ɓ'ϣk׮;0atӿJXh6l؀xׯzԄ=h۶TX\\\僓8fFL&F]X> Ʉl۶ ;vDytʐ#ȟCxzl0(8X&V $hEQ 8qFrܹsCHHFѣGcȐ!^idYYYob Ehݺ3xii)6n܈ne IDAT;wsΞn25|}Y1AAA!P|~?c>b$H~-s)O@:ҥK1uTHaDGj-9l7%6Qn%FH~Sa"DCZݻ~z?9r$FaÆ!44ԓt%|wϰqFDDD ::C Axxe0Aa޽hٲx1qDDDD8D.Q ])+SvƀW\`@bbh(GlF2KvgWW׽G~| /@b:LhDG|$H>$jsk׮!//ׯ Tzӿf۷oGVVV^#GUVF֭a2.ͮ]sG-Y=ƎXÐ}|qڔhZ8}4x)Q 8ʋ%"7M] Mk4l6cZ PQ[&֭[Kn… qU\|:t@zz:Э[7t &ӿ&#''[nƍSN!..CJJðDIOO֭[#GcǎQ?>(oߎǣE;w1).e wzK^#>* $FrG"' b6QPPT.]Bhh(:u޽{W^ѣڴi(OLqq1<`޽صk;ٌ8L&"66ᄊi,ZVsQZZZ]va׮]Xr%F_5 ٌ%K`֬YԩP;< JpGĵ]GzF}ov9LMɯn_+Gw዁ǧ INY-KŴZ>,OC< ɃjuXv5iiإG۶mѱcGt m۶E۶m&;mۑ9s9998q=ʕ+~&Fለ@HH'aZ-70G~cǎaӦMHOOǒ%KXebܹ C~ #88XZ)˧NJN{ɿK.aҤIغu46eqVC1 ?F4U^( EYӣR#r4H=~:_.B:uGqq1BBBXh񈏏Պ:$$!(ȃʶ/b^.vvލxz"^_=fߎVaA^k#'_M'ry@wV)2_#R={{E^^f̘gy5XHAAV^̙3HJJBǎ#c4>) ˑ )sۉkr$FYl \,?b^6m|Z o6ƏGypΝ_|+V`HLLbA6m0p@ad2I_FGAO谠6%(e /{A5Gv>~%8؉ZRQR8RSaR{ 1B6*$ٹsgܹaaaZv; ;Qĕ_+?ժ&8Q[Q[ ;jSwg\jKmD)::W^Ç1a L>&LTVVc͚5qej -[ɓa0PPP,IG5wg\ F|ǏG~`!wcZS B6 :Jڈ<exR~]tZ2oMA~Ζ*/jVW8Hx}st:TTT.Y`` JKK3_GDDo߾#88vNp tv$[⧟~O?ǏEGrr2bbbFGGo^Gdd4%_e4`$WYM`HKKC``~YD*~^.*#Big0jQ{^nGbb"^]-M}[s4HSmGzE>j܍V}lNWЈ)3Q4-B8Չ8I`` ڴi #++ V˗ѻwo5 CEZZ7c_m6o(**B\\BBB}JEQ;ͻusΡK.RБ}C q;o* 8ŵeR)G5?j\Y+Q+:((Nw" oiNF Z())A^^VZ>hݺ5n}A^ ?v;pAdggcϞ=ؿ??J ((aaaHOOGXXȟ<(`~ȑ#>aaa.l6#//O;wƩSp9!<<\ CDDtbQ~:0{*;իWbƍ( E}O5 AAAf1߄B|tPGD-9^Sa 8O>$m; >BPu++k!j%R_yIJ|Wyy9c4Vmwo P&"l]~W^Ů]eׯ_Gee%nG˖-ѲeKivpppIl*<"''ΝC~~>.]2i ɄdxyjkP;;|G#..8wRRRi믿K. >(~cVy͐ !Φ’j*88iiiطoƎ[cY29[(ȯݵF^oD$v,V7LSݫ@qq1JKKq%H'FD[ˡ[i(nW;ʀ'biRTTTH&}`0d2!&&F**vrnjK 0$СCc 0J͛q76ob&YA2GinW[>|pl۶ &MR]ꌰZn$F=bʯzhśaee4"FFmKyQܼRm1e^e$YxQ4S|}EMeHU=#** qqqF p9(GYE ʓתmy 5j3glGزe ^>PօaF-oT6@^բRjBxjvVVV9R[mAM?HY 7 JrGmj[8 HBhg둚-[`ܸqn5sADEE1Pa!66b_SNU/S A"p7\1*63.j#@ʯk*~ԧmĈ̔U% Wvt&jy;QۨOu'U+˯8ܦvvgʠj*n蹺Q#gaKmTJ-Џ5 wyg]m޼Çt3ȇ|ڙ3g|O7Q(\Ը9G-M}舘TK-L"g BQ"eAxZmINWjxv;sNnݺICkݺ5֭[;z)f5?Zn Ʉ{nN6"i+WFvԦ8۲ϢvggSJ@ *FR+lWuwXv-zZ=Ξ=RjT ?O|5մ?zVs Պ]iZP E5];{|u[נ̨QW_y.eNԄjz+WJ9(ЕSFzԃ)[nE~8J ?ި 0BaaG^'3%Oa!0p@ٳ%%%n Q4hǂ?S~ bӦMn QԗlݻѿFm" ㏍;wDa4~|QNhݨ)/$۷/rrrpEO7Qyb=z4֮] `zj ~T|QV`W\ANNz O*"C Q^^5ڲe ӡ<=HdBrr2n5>T09EC b?75 ?DN|`@ZZ6n-++þ}% "'l\zM!jT 1sNKT ?DNt:xذaBԨ"pʋj|Qrr2p)='ᇨ!_͆#?d0U#99tS;Ͼ}ЪU+DFFᇨ~~~>|8Gddd`Æ Z~.PSCT/B}z?55 ?D5`*wM}eeeaРAnh{0ՠM6Ɓ<FsIh4i="r"p|Qzz:9˗/98EM XCHa ߇"" 6 YYYnYBԜw#?1 22۷ǎ;<FNtΟ?d7~~\ĺE:t^k-[ == jia$r~WuS^T1oݻQRR5N}ؙ*" bӦMn Q>|8n݊rSZZ_~ 2a!/ A^cv؁^z! [FT7 ?DU5e ?Dзo_ŋn Qm3)c! <~!͛Ddgg{YD o߾8w[̚5 ]vEAAr㡇§~[2PgnQs_cٲeذal6-´i<:a۱l2Z 7nNCEE, ߏݻKh4CPPf30h ;hٲ"lsnQso>zUn E-<"]QQ DLLtp D zgyF}݇hf3nKLL0| Fb!rѫD+**~ȫi4|UW9mENm0矫~h $"i*oflQËǿ/ 鶈*֭4Bd0pr;5I ?Dо}{;'`{UD o  yжm[x뭷Db!fk}y0p@t:DEE9j5˅ J#-[֭[zժU dߧ&+Dm8X-Z@pp콡9{D z=V^dDEEU[FQQOzmSCa!>ᇼjbAyy ˡ@{ľϾOC^C~Z(//7Rcgߧa!! =+++aX`6O㏑$|xS_5Cn1tP|wXd z聽{JEno/"FׯPVV2X,TVVfyUmNC 6.\r%F%K>Ol6V+***PZZC qE޽;֮] F .]v7n>sM7݄SNaǎFNNq=wީw߇7mڄP 88N_}w0L̄NCyy9SN!??.ݧ fÑ*b_}Z,tP{ҿ-Z<|Ʉ TYN\_]ïZ}~)a!rСCqa:tHݹ}C^^N>}6HQcqW{CԔ0Wiu:z쉞={V{-Z}3~~~֟|S]TiY4^'M 򃿳7}ž*_'| uCAf"VY}ǾOT{^~`?O筟|x't[p7}OT{^~njd2yeؼqHfOl>}"Wxu~sS9x#c'oj'׆y__̇yqW?s\jlFYYEMg7Ʃy>+6Q)8O@^#a'oT]'r׆AW|&-o{jGx}c'_\GY`Z=,z\G{Ap;F?>y>Qu6TF@܉/jɛU\=|vOʝOT ?!oSSf'o>MᇈHᇈ| """) ?DDDS~ȧ0Oa!"""tTTTVX*--EEEL&DǡЮ];h4q%%%t rfeee`FDUqLJ|GGxx8VX~׿ί"<<;vs54łs"11<{Dbb"̙ǭZ 4iRmzw?nY8㣞~i;n ̚5 :Zh~IIIv8>gƅ xb</ ^ OT_'Noٳп,YwWQi/t]w!""˖-Ö-[j2W{V9ϥKpalZv ͆Jݻc„ _ޏ#7߄n? rrr0b ..={o[ƶmۤ>CΝ;@*}/k׮F.]7xCz'|NX\xѡ >, &Ok׮*++_Ċ+^Q㣦NCÙ3gΠO>ظq# ^x;wO?^{ 9990KJJPXX(}vXVdeea+ӧl6gիp-ŋxǥܱc͛`̟?Ϳ5iii˗/ N8Q>۷ǔ)SRQ{=_V=˽ t̙~ /2V\+WԩS~85SO=M6ǢEжX|ga61uTl6{ON /v;^uom۶}݇Kn}]:aLJu_QVVG}\? UO4VC-Z/P 4l0ۢtR#ݻcڵOTsqvtM.o:x@AAr N<,Zm۶;kbqE8pK.wɄkb̘1ظqbW^y1~l"t:}h4+W"77Z00}t\p_|?22什Psa_JxofIDJKKCǎqu`֬Y{qDt0W_E||<.]{ڵނ$&LފvF bĈ3f Oxlڴ k֬ARR[f3X,,XZ|HMM7;=zpkyꩧ_F>}0ydtAAA8r[ڵ x8"j:~… ~* C͛1c ,X6 ۷{g`0 ni_W8p?,ޫW/̜9/?bΝ܈`!$&&رc8t233qQٳGDD2qi""5?DDDS~ȧ0Oa!"""CDDD>ᇈ| """) ?DDDS~ȧ0Oa!"""CDDD>ᇈ| h(~~~DOsj#?? ???h4^#h45jGCFF{9D/i_%}6}xO" 5gZVz}Y>þOުO ?~q'|B\z%%%())lFEE*++=dga̘10LnJ'N``dBHH"##7?~X|9&O NW|22331qDhM|%lܸ۷/BCCD5iz]n"(EBC`0Vz=V+*++a=z2{)Mzш`!00tjjU ?GEE A||>}`ݸ[ {K7w-܂zOEէ«ÏfC`` l6v;4tb؟~FJJ V\'Zq`p,_"7~;qާ^̘1&Mj#111ڵ+?>6m½ދ^z\G c TL'jĐT7 F o&_.t:IXcZѢE DGGy1a^<[DGGزe /^8L>۷sյkEVCdL [nC<ݔ&O9M@BeľJKK 3>(ƎiӦM6nr-`رXj}Y 0?8\~}"WxurΠbffIzzSNᦛntS<|`Ĵ|?ѫW/CBB0}t,^/v7zwu/^oSL#q_H+\|qǎ7ߌÇ{)͊8+wnvϾp;4<˖-êU7k4`̙8q^{5?ԧ `τ%~ ݎP ,,iv1Ͼ> 矑`QYY.]`ѢE1bDNc 88o&j| Mf^礤`߾}:tnŋhٲeBh4kx1rH޽{|rq߿?Ν$O7| wt3 qˮ54iv;VZX4 ^?~{Fjj*f̘˗/{i~ѥ`Ϟ=nQ\pV˷yl"003gđ#G`бcG(//tȋ1Py|6 >mڴe5STT}]ر;wDǎb ֧Q`F׵kW>}n Q5̝;sAYYYncj߾= |Xp! 6xYe~iZt tSO>8p ,XШ ؾ};fΜ|ƍ=, ?^~e書zj'L4 FFF|.\tc!`5w ?;vĉk5k{NÌ3p1DDDK.xQRRQ3Cj<~_ᅬ{=%44:~7  `мb[o ~SܹsTe|GHMMEFвeK̞=f#*?Z }ʋ䄨(|Bb1h"ܾ}/_D@@֯_bC#"*JP1$?0vX<|'N:Rzul߾OF||<~""wwwԨQæ'ldIɏD"ʕ+)t()((عs'֬Y={V谈(!y?YR:tHH,WXX.]ӧcׯ"(C%?"Vܹsi7D6lRRR"44'O_%thQCEb,-Ԯ]111BbgݻJ QPP thPCEbQzuвzj,^u?͛7Q~}ر.°A[prrɈUnشiСXub߾}~Ǐ:,R(!6+fZΜ9˗cԩڵ+]&tXPCG~5d:6l:շo_ܺu Cn0zh<~X谈QCGbM,Éۑ)t(VK"`ɸ{.jժ͛c޼yxС3*?ĚXCSfML0QQQBbܰ|rܼyYYY-[P("8";;[P)5$?0w\:tw:Jf͚šQF8ra#QC,UgRi֬c۶mBhh(.\ tX@@~M3g*r &LbРAHMM:,'J~E֔899!** СTJb~!ݻVZ!$$ӦMCNNС2PC,U~ƍ#>>^P*-Ts"99* Xz5 bիLlݺ1'tXhG"`ʕ:Jjժؼy3Ο? .aÆ:4R%?DPqqqW!۷Ӱ:# :?ѲeK7χU*T!:'8\S@#!6bH$X,xpJ&CĶ$''UVd=<#ô* %> 6uwBC.k#o߾| pvv8*-\_-d߿/`DDaaa?1}t :G~~>d28իW Z C5 IDATՠ9?DW\AV9"wޅ3D{{{*)۰@.Vy=y$z|H$ic2e *RC߾}jCsChхKJ%r9&{Vf2 vvvŐH$a!n} cpuu5?.IѱcG$$$ <<Ϟ=Cfff`MKѣGZj=tP>}[x<(1PEWqq%?g~,Un_bƌf9j}s,Iԩ_57Z^¼Ν;"CR!??-((@DD___ YPѠ~K&!??yyyEN , ۷oٳg!)ShLd8v= -Zh'336l۷QJOO;D8u<<<WWWHR~}PPkPeggիW_mWV%)hy yRRRd\zoǏQXX5jEXB%?EEE|KK. ^zرch۶-J%.]֭[#!!_k׮HHH{g4KOOG- DdffbҥXp!Ze/H~ + JaooV"kiI %D!++ rUV- 4}O~.yn,gϞűcW_?|?~<~g 4=f;;;D"q JRckQQzI}3jGmݺu\ ;v@ u x="""P(yHcnm#$X/ʴSmI Z^4 G)US ޽;yf~rww:t{ׯׯddd`׮]hӦ n\G=S$!%B[d2}sU,s`:E4_-1ooo|=z4x96mڄZjA&ɓ'V5hSСCok׮CFFqYлCЩ/Q>u᪙<ի־$-/a:I=P?{:uBΝ!JqFddd 33{$􄝝?wڵk8}4z-[Fnn.VZzcďR>ud(**\.vK^QY(((7n6P凔-[pڴiHIIA`` ~?c˃RD*U'NT*EJJ $uʕ?>/^ ooo!?e2 HR\\ddeenݺ_G1^*arj`EEEJP?)'OD`` @Æ T*Ń4~ԩS999qF4zh#GjT_Z5|4ݝxxx`3.-[ ZɓѰaCo䛃rU}?կ&K䜩Bd2QѣGqUEEE{Vߔ(!G* >k\pJ'..\]]aggwj%:C*S?r~NQFFڵk={"%%Kbݻw34lF3o%)&L=z?ՕO\\\жm[[Nc|]t AAAĉѾ}{ԩS?Q0PCŀ'"11X`1|pOiݾ}sӧOq X~)<==舞={⧟~ºu됕c￯W;0!<<<0tP( !77˖-… 1vX'lق1cUGcoL>O ~z,_k׮$%%!44{ƭ[/^`ƍ5j:dkAA4燔Ç#55|/H0w\5 гgOlذgƚ5k ѻwoYoÇ~Yfa֬Y߿5jлRy޽... ˗QPP]jh^˘}S(ڵkƍΝ;9sm۶Xz5= /^u0䇔Eaԩy& 4iuIԩS1~x\|AAA4jժ8z(WJ:u*233q TLJ~J&6_*Y>JSERHm۶!,, 5kČ3PZ58q}ƌ ,Wظq#VX/7^>LHE`rC]f=[=W}}[U{]:v]va֬YذaD"0sL,[Lݜ9sk.t۷oGfp=|pssôiӄT kF'?SR 777sG,-y.bzsIC%+hsׯw[#GСC8bcc1a$,, Ν! d^:ܽvX#9>tڏU K&Hh6+zΝ;5~NLL6mdffѣG%*[F%?%>~5Bq_4]_>4b|4_O5N{qB@aa!%?RPTiG%ќc h!ä[b^.H&X!.9o2G+fM(, ggn%?М7bN7+9ڿԫTUI,m>*bIs~! *>U}sw:nk.)..X,UQh>LKRV.+.B+Qs3d]wA"/͇oF'?%J.***2glX]漑|3uK*QkKF!tf7SP+.B'Nyqhz֗"Θ֌ar*UR_BQ4獔77ӵ/%:1fJ~tZS`X;XOTڤS_4獘f%_R:&%VJ~He>J4T}*;%+EX7}3:L H4獔77S#DbF̩fez)!sߌAO * ϟ?G~~СٳgHOO9'yyyHOO4獔}TCHe7cQSBjj*пC۔)SPvmܸqC +Tvsٺu+j׮~\گh4獘>8\b?UIeTt)A;w>}vb9d2?n+,@%X }նY[4hB=zTP2~xxyyŋBbT*+O=#,LL~oiiiƀm| 푔s'N4k׮^\W#G~eG*U?|1bѢE x{{#$$0񰷷Dž h֬Y ""D>}퍖-[ƍyK#,, Eݺud:25h׮С @JW=zF0dL5y!Zjϟ>@hh(N:GbĉUzR\˭h"9rhݺAByzӧٳ'd2f̘֭[#33[la^zi3j(dddwFll,ЧO\x={6vڅ'O`ɨSN˗Eǎ!H0sL#Gĉ| ûヒlt w/_US*u? f͚K$H$oQ\\\nX?ƣGPV-Ԯ]!''זdwr9lp* nBڵaTwBaFP(G7nSNC1#,w#F`؂ 4_bX^c0K.>'Nd#7ݫW2wZ]|`Cܹ`s޽{L$aÆۆ 0___o/**b~~~L$<~{v}K;0^z˗<ְaC>|X|r;0X СCԩSƍÇ,''d2P(cׯ_Ϝ?~3~x.^c,++I$6l0vEֱcG}cM.BR,R)kٲa;CƁ3l򓓓={/x_~)ڎ:.\-Z*?0]C+?iiiۛ`իWg'OfdZU~~W>ckc*?W^j֭[ k߾=cx  _1*iJ`:uT*eϟ?>rHt/`7nt<==5?~`&M OXgYSTh0TYRRU~\ 4mԤvUV.2nݺHHH PXX[bРAZ*}ܽ{̩AZ۸#j2QF mT^|r[nB'|utRYYYel2Is[zUR}Uq*UAs`Æ ~FSܹ#F___<E}̜93f̀ϟŋ# freUIoܸU%է]C*ظq#bbb0rH;vLkY0g( L2}/Y &N{qk}ٵMMMչ=00۷oƍqy?~ѣGwΎ߿>OIHNN͛Qzu>}O꣢мys$$$.3gΠo߾ػw/`vKTX~r1cݻq] %%6l?ϟݻԩS6)MT~6l8VիWhܸQҮ#dmT*!Jh$%%^z>ܹcpƺyֶ˗/xKtV^ZZ/sTuUʫ2oUJ`qq1/u]v-oߎwy񈌌DVח>>}HԪU {ӱ{nY52amUR@͛R0o<R)V\ikDh&m6ܾ}M4)v4h'''3335믘9sV/H p^^^ BT\\vXn)bDGGxiӦ(,,ıc4~ڵkjv)2U%?8z(uRip%ZnҪH$„ pEܿ~-zOb̙y9r$֬Y#..999q!TRI(yQolØ*>fsXf \///4k֌]~бcG6iK3B"`ذaXbߏcpww^J$mڵ+mۆ  ,,?nܸqD9r$v܉+W 66cƌARR""" L:v*s;z~a׮]سgq%;!n:D" >˗/DZe{_QVv\ɓZϭ_kƁʵ*r\딐UIC+vvvzZnΝ;|q_~-[>`(M^^?5jয়~B^䬨,"kkJ _|S]$?:t' `Ȑ!8pPg{E-pYL4 +V?߯nǎea̘1hժ_ֻ{nÇ39/ .]‡~#F !!dž 4ҥ 9{{{,\ݻwǔ)S0rH,Z+Ç4&Kd2|HOO7괬!rGϟhcZ }T*̚5 szp9br7OZ7RݺuΛꪄgEV,T)U(r"fub|Jb,%%) oɓ'?`ex>nݺexe2KNNfϞ={vR/^0JŮ_Ύ9qY.^b,66V圖 -rح[7uڕ޽EGGKK=,,̤vO:/9o^xgϞӧ8p d K ND8tUoooNNNppp]/_ʕ+pttDFt+vq ( 4mTkyu999~:y#GDll,@͚5qI"Q凼Q\\o >\BuЕbckW,Ļz*4hf͚a5C,k[BHy*)x.[ K !BJ~!bc(!BMB!6B!J~!bS(!BM1*vJ$iw"{NHeq` *?bTH"O,M?17} F) 87`gg1uOM߾ǡ>H*#Cǁ1E#;;;NT%L"G\_.yDM} FecN"Dnn.^xH9Ɓ)R^(ܠFc4E<*WfuppO%p\D”ǡ>H9ƁL,T2WjG,ꃏ%҅?19 V0*IqGtA,]E xG#ŵS۠>H9ƁLԝ1X JŗAǡ+ %*9ˇ?1}OZkä_vvv,mk> K}X;SƁLN~"[A !)z!BB!6B!J~!bS(!BMB!6B!J~!bS(!BMB!6B!ؔ(IENDB`circus-0.12.1/docs/source/design/index.rst000066400000000000000000000001351256046442300204320ustar00rootroot00000000000000Design decisions ################ .. toctree:: :maxdepth: 1 architecture security circus-0.12.1/docs/source/design/security.rst000066400000000000000000000112621256046442300211750ustar00rootroot00000000000000.. _security: Security ######## Circus is built on the top of the ZeroMQ library and comes with no security at all in its protocols. However, you can run a Circus system on a server and set up an SSH tunnel to access it from another machine. This section explains what Circus does on your system when you run it, and ends up describing how to use an SSH tunnel. You can also read http://www.zeromq.org/area:faq#toc5 TCP ports ========= By default, Circus opens the following TCP ports on the local host: - **5555** -- the port used to control circus via **circusctl** - **5556** -- the port used for the Publisher/Subscriber channel. - **5557** -- the port used for the statistics channel -- if activated. - **8080** -- the port used by the Web UI -- if activated. These ports allow client apps to interact with your Circus system, and depending on how your infrastructure is organized, you may want to protect these ports via firewalls **or** configure Circus to run using **IPC** ports. Here's an example of running Circus using only IPC entry points:: [circus] check_delay = 5 endpoint = ipc:///var/circus/endpoint pubsub_endpoint = ipc:///var/circus/pubsub stats_endpoint = ipc:///var/circus/stats When Configured using IPC, the commands must be run from the same box, but no one can access them from outside, unlike using TCP. The commands must also be run as a user that has write access to the ipc socket paths. You can modify the owner of the **endpoint** using the **endpoint_owner** config option. This allows you to run circusd as the root user, but allow non-root processes to send commands to **circusd**. Note that when using **endpoint_owner**, in order to prevent non-root processes from being able to start arbitrary processes that run with greater privileges, the add command will enforce that new Watchers must run as the **endpoint_owner** user. Watcher definitions in the local config files will not be restricted this way. Of course, if you activate the Web UI, the **8080** port will still be open. circushttpd =========== When you run **circushttpd** manually, or when you use the **httpd** option in the ini file like this:: [circus] check_delay = 5 endpoint = ipc:///var/circus/endpoint pubsub_endpoint = ipc:///var/circus/pubsub stats_endpoint = ipc:///var/circus/stats httpd = 1 The web application will run on port *8080* and will let anyone accessing the web page manage the **circusd** daemon. That includes creating new watchers that can run any command on your system ! **Do not make it publicly available** If you want to protect the access to the web panel, you can serve it behind Nginx or Apache or any proxy-capable web server, that can take care of the security. User and Group Permissions ========================== By default, all processes started with Circus will be running with the same user and group as **circusd**. Depending on the privileges the user has on the system, you may not have access to all the features Circus provides. For instance, some statistics features on a running processes require extended privileges. Typically, if the CPU usage numbers you get using the **stats** command are *N/A*, it means your user can't access the proc files. This will be the case by default under Mac OS X. You may run **circusd** as root to fix this, and set the **uid** and **gid** values for each watcher to get all the features. But beware that running **circusd** as root exposes you to potential privilege escalation bugs. While we're doing our best to avoid any bugs, running as root and facing a bug that performs unwanted actions on your system may be dangerous. The best way to prevent this is to make sure that the system running Circus is completely isolated (like a VM) **or** to run the whole system under a controlled user. SSH tunneling ============= Clients can connect to a **circusd** instance by creating an SSH tunnel. To do so, pass the command line option **--ssh** followed by **user@address**, where **user** is the user on the remote server and **address** is the server's address as seen by the client. The SSH protocol will require credentials to complete the login. If **circusd** as seen by the SSH server is not at the default endpoint address **localhost:5555** then specify the **circusd** address using the option **--endpoint** Secured setup example ===================== Setting up a secured Circus server can be done by: - Running an SSH Server - Running Apache or Nginx on the *80* port, and doing a reverse-proxy on the *8080* port. - Blocking the *8080* port from outside access. - Running all ZMQ Circusd ports using IPC files instead of TCP ports, and tunneling all calls via SSH. .. image:: circus-security.png circus-0.12.1/docs/source/faq.rst000066400000000000000000000065351256046442300166330ustar00rootroot00000000000000Frequently Asked Questions ########################## Here is a list of frequently asked questions about Circus: .. _whycircussockets: How does Circus stack compare to a classical stack? =================================================== In a classical WSGI stack, you have a server like Gunicorn that serves on a port or an unix socket and is usually deployed behind a web server like Nginx: .. image:: classical-stack.png Clients call Nginx, which reverse proxies all the calls to Gunicorn. If you want to make sure the Gunicorn process stays up and running, you have to use a program like Supervisord or upstart. Gunicorn in turn watches for its processes ("workers"). In other words you are using two levels of process managment. One that you manage and control (supervisord), and a second one that you have to manage in a different UI, with a different philosophy and less control over what's going on (the wsgi server's one) This is true for Gunicorn and most multi-processes WSGI servers out there I know about. uWsgi is a bit different as it offers plethoras of options. But if you want to add a Redis server in your stack, you *will* end up with managing your stack processes in two different places. Circus' approach on this is to manage processes *and* sockets. A Circus stack can look like this: .. image:: circus-stack.png So, like Gunicorn, Circus is able to bind a socket that will be proxied by Nginx. Circus don't deal with the requests but simply binds the socket. It's then up to a web worker process to accept connections on the socket and do the work. It provides equivalent features than Supervisord but will also let you manage all processes at the same level, wether they are web workers or Redis or whatever. Adding a new web worker is done exactly like adding a new Redis process. Benches ------- We did a few benches to compare Circus & Chaussette with Gunicorn. To summarize, Circus is not adding any overhead and you can pick up many different backends for your web workers. See: - http://blog.ziade.org/2012/06/28/wgsi-web-servers-bench - http://blog.ziade.org/2012/07/03/wsgi-web-servers-bench-part-2 .. _troubleshooting: How to troubleshoot Circus? =========================== By default, `circusd` keeps its logging to `stdout` rather sparse. This lack of output can make things hard to troubleshoot when processes seem to be having trouble starting. To increase the logging `circusd` provides, try increasing the log level. To see the available log levels just use the `--help` flag. :: $ circus --log-level debug test.ini One word of warning. If a process is flapping and the debug log level is turned on, you will see messages for each start attempt. It might be helpful to configure the app that is flapping to use a `warmup_delay` to slow down the messages to a manageable pace. :: [watcher:webapp] cmd = python -m myapp.wsgi warmup_delay = 5 By default, `stdout` and `stderr` are captured by the `circusd` process. If you are testing your config and want to see the output in line with the circusd output, you can configure your watcher to use the `StdoutStream` class. :: [watcher:webapp] cmd = python -m myapp.wsgi stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream If your application is producing a traceback or error when it is trying to start up you should be able to see it in the output. circus-0.12.1/docs/source/for-devs/000077500000000000000000000000001256046442300170465ustar00rootroot00000000000000circus-0.12.1/docs/source/for-devs/adding-commands.rst000066400000000000000000000067351256046442300226400ustar00rootroot00000000000000.. _addingcmds: Adding new commands ################### We tried to make adding new commands as simple as possible. You need to do three things: 1. create a ``your_command.py`` file under ``circus/commands/``. 2. Implement a single class in there, with predefined methods 3. Add the new command in ``circus/commands/__init__.py``. Let's say we want to add a command which returns the number of watchers currently in use, we would do something like this (extensively commented to allow you to follow more easily): .. code-block:: python from circus.commands.base import Command from circus.exc import ArgumentError, MessageError class NumWatchers(Command): """It is a good practice to describe what the class does here. Have a look at other commands to see how we are used to format this text. It will be automatically included in the documentation, so don't be affraid of being exhaustive, that's what it is made for. """ # all the commands inherit from `circus.commands.base.Command` # you need to specify a name so we find back the command somehow name = "numwatchers" # Set waiting to True or False to define your default behavior # - If waiting is True, the command is run synchronously, and the client may get # back results. # - If waiting is False, the command is run asynchronously on the server and the client immediately # gets back an 'ok' response # # By default, commands are set to waiting = False waiting = True # options options = [('', 'optname', default_value, 'description')] properties = ['foo', 'bar'] # properties list the command arguments that are mandatory. If they are # not provided, then an error will be thrown def execute(self, arbiter, props): # the execute method is the core of the command: put here all the # logic of the command and return a dict containing the values you # want to return, if any return {"numwatchers": arbiter.numwatchers()} def console_msg(self, msg): # msg is what is returned by the execute method. # this method is used to format the response for a console (it is # used for instance by circusctl to print its messages) return "a string that will be displayed" def message(self, *args, **opts): # message handles console input. # this method is used to map console arguments to the command # options. (its is used for instance when calling the command via # circusctl) # NotImplementedError will be thrown if the function is missing numArgs = 1 if not len(args) == numArgs: raise ArgumentError('Invalid number of arguments.') else: opts['optname'] = args[0] return self.make_message(**opts) def validate(self, props): # this method is used to validate that the arguments passed to the # command are correct. An ArgumentError should be thrown in case # there is an error in the passed arguments (for instance if they # do not match together. # In case there is a problem wrt their content, a MessageError # should be thrown. This method can modify the content of the props # dict, it will be passed to execute afterwards. circus-0.12.1/docs/source/for-devs/index.rst000066400000000000000000000031721256046442300207120ustar00rootroot00000000000000.. _fordevs: Circus for developers ##################### Using Circus as a library ------------------------- Circus provides high-level classes and functions that will let you manage processes in your own applications. For example, if you want to run four processes forever, you could write: .. code-block:: python from circus import get_arbiter myprogram = {"cmd": "python myprogram.py", "numprocesses": 4} arbiter = get_arbiter([myprogram]) try: arbiter.start() finally: arbiter.stop() This snippet will run four instances of *myprogram* and watch them for you, restarting them if they die unexpectedly. To learn more about this, see :ref:`library` Extending Circus ---------------- It's easy to extend Circus to create a more complex system, by listening to all the **circusd** events via its pub/sub channel, and driving it via commands. That's how the flapping feature works for instance: it listens to all the processes dying, measures how often it happens, and stops the incriminated watchers after too many restarts attempts. Circus comes with a plugin system to help you write such extensions, and a few built-in plugins you can reuse. See :ref:`plugins`. You can also have a more subtile startup and shutdown behavior by using the **hooks** system that will let you run arbitrary code before and after some processes are started or stopped. See :ref:`hooks`. Last but not least, you can also add new commands. See :ref:`addingcmds`. Developers Documentation Index ------------------------------ .. toctree:: :maxdepth: 1 library writing-plugins writing-hooks adding-commands circus-0.12.1/docs/source/for-devs/library.rst000066400000000000000000000043151256046442300212470ustar00rootroot00000000000000.. _library: Circus Library ############## The Circus package is composed of a high-level :func:`get_arbiter` function and many classes. In most cases, using the high-level function should be enough, as it creates everything that is needed for Circus to run. You can subclass Circus' classes if you need more granularity than what is offered by the configuration. The get_arbiter function ======================== :func:`get_arbiter` is just a convenience on top of the various circus classes. It creates a :term:`arbiter` (class :class:`Arbiter`) instance with the provided options, which in turn runs a single :class:`Watcher` with a single :class:`Process`. .. autofunction:: circus.get_arbiter Example: .. code-block:: python from circus import get_arbiter arbiter = get_arbiter([{"cmd": "myprogram", "numprocesses": 3}]) try: arbiter.start() finally: arbiter.stop() Classes ======= Circus provides a series of classes you can use to implement your own process manager: - :class:`Process`: wraps a running process and provides a few helpers on top of it. - :class:`Watcher`: run several instances of :class:`Process` against the same command. Manage the death and life of processes. - :class:`Arbiter`: manages several :class:`Watcher`. .. autoclass:: circus.process.Process :members: pid, stdout, stderr, send_signal, stop, age, info, children, is_child, send_signal_child, send_signal_children, status Example:: >>> from circus.process import Process >>> process = Process('Top', 'top', shell=True) >>> process.age() 3.0107998847961426 >>> process.info() 'Top: 6812 N/A tarek Zombie N/A N/A N/A N/A N/A' >>> process.status 1 >>> process.stop() >>> process.status 2 >>> process.info() 'No such process (stopped?)' .. autoclass:: circus.watcher.Watcher :members: notify_event, reap_processes, manage_processes, reap_and_manage_processes, spawn_processes, spawn_process, kill_process,kill_processes, send_signal_child, stop,start, restart, reload, do_action .. autoclass:: circus.arbiter.Arbiter :members: start, stop, reload, numprocesses, numwatchers, get_watcher, add_watcher circus-0.12.1/docs/source/for-devs/writing-hooks.rst000066400000000000000000000125101256046442300224030ustar00rootroot00000000000000.. _hooks: Hooks ##### Circus provides hooks that can be used to trigger actions upon watcher events. Available hooks are: - **before_start**: called before the watcher is started. If the hook returns **False** the startup is aborted. - **after_start**: called after the watcher is started. If the hook returns **False** the watcher is immediately stopped and the startup is aborted. - **before_spawn**: called before the watcher spawns a new process. If the hook returns **False** the watcher is immediately stopped and the startup is aborted. - **after_spawn**: called after the watcher spawns a new process. If the hook returns **False** the watcher is immediately stopped and the startup is aborted. - **before_stop**: called before the watcher is stopped. The hook result is ignored. - **after_stop**: called after the watcher is stopped. The hook result is ignored. - **before_signal**: called before a signal is sent to a watcher's process. If the hook returns **False** the signal is not sent (except SIGKILL which is always sent) - **after_signal**: called after a signal is sent to a watcher's process. - **extended_stats**: called when stats are requested with extended=True. Used for adding process-specific stats to the regular stats output. Example ======= A typical use case is to control that all the conditions are met for a process to start. Let's say you have a watcher that runs *Redis* and a watcher that runs a Python script that works with *Redis*. With Circus you can order the startup by using the ``priority`` option: .. code-block:: ini [watcher:queue-worker] cmd = python -u worker.py priority = 1 [watcher:redis] cmd = redis-server priority = 2 With this setup, Circus will start *Redis* first and then it will start the queue worker. But Circus does not really control that *Redis* is up and running. It just starts the process it was asked to start. What we miss here is a way to control that *Redis* is started and fully functional. A function that controls this could be:: import redis import time def check_redis(*args, **kw): time.sleep(.5) # give it a chance to start r = redis.StrictRedis(host='localhost', port=6379, db=0) r.set('foo', 'bar') return r.get('foo') == 'bar' This function can be plugged into Circus as an ``before_start`` hook: .. code-block:: ini [watcher:queue-worker] cmd = python -u worker.py hooks.before_start = mycoolapp.myplugins.check_redis priority = 1 [watcher:redis] cmd = redis-server priority = 2 Once Circus has started the **redis** watcher, it will start the **queue-worker** watcher, since it follows the **priority** ordering. Just before starting the second watcher, it will run the **check_redis** function, and in case it returns **False** will abort the watcher starting process. Hook signature ============== A hook must follow this signature:: def hook(watcher, arbiter, hook_name, **kwargs): ... # If you don't return True, the hook can change # the behavior of circus (depending on the hook) return True Where **watcher** is the **Watcher** class instance, **arbiter** the **Arbiter** one, **hook_name** the hook name and **kwargs** some additional optional parameters (depending on the hook type). The **after_spawn** hook adds the pid parameters:: def after_spawn(watcher, arbiter, hook_name, pid, **kwargs): ... # If you don't return True, circus will kill the process return True Where **pid** is the PID of the corresponding process. Likewise, **before_signal** and **after_signal** hooks add pid and signum:: def before_signal_hook(watcher, arbiter, hook_name, pid, signum, **kwargs): ... # If you don't return True, circus won't send the signum signal # (SIGKILL is always sent) return True Where **pid** is the PID of the corresponding process and **signum** is the corresponding signal. You can ignore those but being able to use the watcher and/or arbiter data and methods can be useful in some hooks. Note that hooks are called with named arguments. So use the hook signature without changing argument names. The **extended_stats** hook has its own additional parameters in **kwargs**:: def extended_stats_hook(watcher, arbiter, hook_name, pid, stats, **kwargs): ... Where **pid** is the PID of the corresponding process and **stats** the regular stats to be returned. Add your own stats into **stats**. An example is in examples/uwsgi_lossless_reload.py. As a last example, here is a super hook which can deal with all kind of signals:: def super_hook(watcher, arbiter, hook_name, **kwargs): pid = None signum = None if hook_name in ('before_signal', 'after_signal'): pid = kwargs['pid'] signum = kwargs['signum'] ... return True Hook events =========== Everytime a hook is run, its result is notified as an event in Circus. There are two events related to hooks: - **hook_success**: a hook was successfully called. The event keys are **name** the name if the event, and **time**: the date of the events. - **hook_failure**: a hook has failed. The event keys are **name** the name if the event, **time**: the date of the events and **error**: the exception that occurred in the event, if any. circus-0.12.1/docs/source/for-devs/writing-plugins.rst000066400000000000000000000106221256046442300227430ustar00rootroot00000000000000.. _develop_plugins: Writing plugins ############### Circus comes with a plugin system which lets you interact with **circusd**. .. note:: We might add circusd-stats support to plugins later on. A Plugin is composed of two parts: - a ZMQ subscriber to all events published by **circusd** - a ZMQ client to send commands to **circusd** Each plugin is run as a separate process under a custom watcher. A few examples of some plugins you could create with this system: - a notification system that sends e-mail alerts when a watcher is flapping - a logger - a tool that adds or removes processes depending on the load - etc. Circus itself comes with a few :ref:`built-in plugins `. The CircusPlugin class ====================== Circus provides a base class to help you implement plugins: :class:`circus.plugins.CircusPlugin` .. autoclass:: circus.plugins.CircusPlugin :members: call, cast, handle_recv, handle_stop, handle_init When initialized by Circus, this class creates its own event loop that receives all **circusd** events and pass them to :func:`handle_recv`. The data received is a tuple containing the topic and the data itself. :func:`handle_recv` **must** be implemented by the plugin. The :func:`call` and :func:`cast` methods can be used to interact with **circusd** if you are building a Plugin that actively interacts with the daemon. :func:`handle_init` and :func:`handle_stop` are just convenience methods you can use to initialize and clean up your code. :func:`handle_init` is called within the thread that just started. :func:`handle_stop` is called in the main thread just before the thread is stopped and joined. Writing a plugin ================ Let's write a plugin that logs in a file every event happening in **circusd**. It takes one argument which is the filename. The plugin may look like this:: from circus.plugins import CircusPlugin class Logger(CircusPlugin): name = 'logger' def __init__(self, *args, **config): super(Logger, self).__init__(*args, **config) self.filename = config.get('filename') self.file = None def handle_init(self): self.file = open(self.filename, 'a+', buffering=1) def handle_stop(self): self.file.close() def handle_recv(self, data): watcher_name, action, msg = self.split_data(data) msg_dict = self.load_message(msg) self.file.write('%s %s::%r\n' % (action, watcher_name, msg_dict)) That's it ! This class can be saved in any package/module, as long as it can be seen by Python. For example, :class:`Logger` may be found in a *plugins* module within a *myproject* package. Async requests -------------- In case you want to make any asynchronous operations (like a Tornado call or using periodicCall) make sure you are using the right loop. The loop you always want to be using is self.loop as it gets set up by the base class. The default loop often isn't the same and therefore code might not get executed as expected. Trying a plugin =============== You can run a plugin through the command line with the **circus-plugin** command, by specifying the plugin fully qualified name:: $ circus-plugin --endpoint tcp://127.0.0.1:5555 --pubsub tcp://127.0.0.1:5556 --config filename:circus-events.log myproject.plugins.Logger [INFO] Loading the plugin... [INFO] Endpoint: 'tcp://127.0.0.1:5555' [INFO] Pub/sub: 'tcp://127.0.0.1:5556' [INFO] Starting Another way to run a plugin is to let Circus handle its initialization. This is done by adding a **[plugin:NAME]** section in the configuration file, where *NAME* is a unique name for your plugin: .. code-block:: ini [plugin:logger] use = myproject.plugins.Logger filename = /var/myproject/circus.log **use** is mandatory and points to the fully qualified name of the plugin. When Circus starts, it creates a watcher with one process that runs the pointed class, and pass any other variable contained in the section to the plugin constructor via the **config** mapping. You can also programmatically add plugins when you create a :class:`circus.arbiter.Arbiter` class or use :func:`circus.get_arbiter`, see :ref:`library`. Performances ============ Since every plugin is loaded in its own process, it should not impact the overall performances of the system as long as the work done by the plugin is not doing too many calls to the **circusd** process. circus-0.12.1/docs/source/for-ops/000077500000000000000000000000001256046442300167065ustar00rootroot00000000000000circus-0.12.1/docs/source/for-ops/circusweb.rst000066400000000000000000000217541256046442300214370ustar00rootroot00000000000000.. _circushttpd: The Web Console ############### Circus comes with a Web Console that can be used to manage the system. The Web Console lets you: * Connect to any running Circus system * Watch the processes CPU and Memory usage in real-time * Add or kill processes * Add new watchers .. note:: The real-time CPU & Memory usage feature uses the stats socket. If you want to activate it, make sure the Circus system you'll connect to has the stats enpoint enabled in its configuration:: [circus] statsd = True By default, this option is not activated. The web console is its own package, you need to install:: $ pip install circus-web To enable the console, add a few options in the Circus ini file:: [circus] httpd = True httpd_host = localhost httpd_port = 8080 *httpd_host* and *httpd_port* are optional, and default to *localhost* and *8080*. If you want to run the web app on its own, just run the **circushttpd** script:: $ circushttpd Bottle server starting up... Listening on http://localhost:8080/ Hit Ctrl-C to quit. By default the script will run the Web Console on port 8080, but the --port option can be used to change it. Using the console ================= Once the script is running, you can open a browser and visit *http://localhost:8080*. You should get this screen: .. image:: web-login.png :align: center :height: 400px The Web Console is ready to be connected to a Circus system, given its **endpoint**. By default the endpoint is *tcp://127.0.0.1:5555*. Once you hit *Connect*, the web application will connect to the Circus system. With the Web Console logged in, you should get a list of watchers, and a real-time status of the two Circus processes (circusd and circusd-stats). .. image:: web-index.png :target: web-index.png :align: center :height: 400px You can click on the status of each watcher to toggle it from **Active** (green) to **Inactive** (red). This change is effective immediatly and let you start & stop watchers. If you click on the watcher name, you will get a web page for that particular watcher, with its processes: .. image:: web-watchers.png :target: web-watchers.png :align: center :height: 400px On this screen, you can add or remove processes, and kill existing ones. Last but not least, you can add a brand new watcher by clicking on the *Add Watcher* link in the left menu: .. image:: web-add-watcher.png :target: web-add-watcher.png :align: center :height: 400px Running behind Nginx ==================== Nginx can act as a proxy and security layer in front of circus-web. .. note:: To receive real-time status updates and graphs in circus-web, you must provide a Nginx proxy solution that has websocket support Nginx >= 1.3.13 --------------- As of Nginx>=1.3.13 websocket support is built-in, so there is no need to combine Nginx with Varnish or HAProxy. An example Nginx config with websocket support: .. code-block:: ini upstream circusweb_server { server 127.0.0.1:8080; } server { listen 80; server_name _; location / { proxy_pass http://circusweb_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto http; proxy_redirect off; } location ~/media/\*(.png|.jpg|.css|.js|.ico)$ { alias /path_to_site-packages/circusweb/media/; } } Nginx < 1.3.13 -------------- Nginx versions < 1.3.13 do not have websocket support built-in. To provide websocket support for circus-web when using Nginx < 1.3.13, you can combine Nginx with Varnish or HAProxy. That is, Nginx in front of circus-web, with Varnish or HAProxy in front of Nginx. The example below shows the combined Nginix and Varnish configuration required to proxy circus-web and provide websocket support. **Nginx configuration:** .. code-block:: ini upstream circusweb_server { server 127.0.0.1:8080; } server { listen 8001; server_name _; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://circusweb_server; } location ~/media/\*(.png|.jpg|.css|.js|.ico)$ { alias /path_to_site-packages/circusweb/media/; } } If you want more Nginx configuration options, see http://wiki.nginx.org/HttpProxyModule. **Varnish configuration:** .. code-block:: ini backend default { .host = "127.0.0.1"; .port = "8001"; } backend socket { .host = "127.0.0.1"; .port = "8080"; .connect_timeout = 1s; .first_byte_timeout = 2s; .between_bytes_timeout = 60s; } sub vcl_pipe { if (req.http.upgrade) { set bereq.http.upgrade = req.http.upgrade; } } sub vcl_recv { if (req.http.Upgrade ~ "(?i)websocket") { set req.backend = socket; return (pipe); } } In the Varnish configuration example above two backends are defined. One serving the web console and one serving the socket connections. Web console requests are bound to port 8001. The Nginx 'server' directive should be configured to listen on port 8001. Websocket connections are upgraded and piped directly to the circushttpd process listening on port 8080 by Varnish. i.e. bypassing the Nginx proxy. Ubuntu ------ Since the version 13.10 (*Saucy*), Ubuntu includes Nginx with websocket support in its own repositories. For older versions, you can install Nginx>=1.3.13 from the official Nginx stable PPA, as so: .. code-block:: sh sudo apt-get install python-software-properties sudo add-apt-repository ppa:nginx/stable sudo apt-get update sudo apt-get install nginx nginx -v Password-protect circushttpd ============================ As explained in the :ref:`Security` page, running *circushttpd* is pretty unsafe. We don't provide any security in Circus itself, but you can protect your console at the NGinx level, by using http://wiki.nginx.org/HttpAuthBasicModule Example:: location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Host: $http_host; proxy_set_header X-Forwarded-Proto: $scheme; proxy_redirect off; proxy_pass http://127.0.0.1:8080; auth_basic "Restricted"; auth_basic_user_file /path/to/htpasswd; } The **htpasswd** file contains users and their passwords, and a password prompt will pop when you access the console. You can use Apache's htpasswd script to edit it, or the Python script they provide at: http://trac.edgewall.org/browser/trunk/contrib/htpasswd.py However, there's no native support for the combined use of HTTP Authentication and WebSockets (the server will throw HTTP 401 error codes). A workaround is to disable such authentication for the socket.io server. Example (needs to be added before the previous rule):: location /socket.io { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Host: $http_host; proxy_set_header X-Forwarded-Proto: $scheme; proxy_redirect off; proxy_pass http://127.0.0.1:8080; } Of course that's just one way to protect your web console, you could use many other techniques. Extending the web console ========================= We picked *bottle* to build the webconsole, mainly because it's a really tiny framework that doesn't do much. By having a look at the code of the web console, you'll eventually find out that it's really simple to understand. Here is how it's split: * The `circushttpd.py` file contains the "views" definitions and some code to handle the socket connection (via socketio). * the `controller.py` contains a single class which is in charge of doing the communication with the circus controller. It allows to have a nicer high level API when defining the web server. If you want to add a feature in the web console you can reuse the code that's existing. A few tools are at your disposal to ease the process: * There is a `render_template` function, which takes the named arguments you pass to it and pass them to the template renderer and return the resulting HTML. It also passes some additional variables, such as the session, the circus version and the client if defined. * If you want to run commands and doa redirection depending the result of it, you can use the `run_command` function, which takes a callable as a first argument, a message in case of success and a redirection url. The :class:`StatsNamespace` class is responsible for managing the websocket communication on the server side. Its documentation should help you to understand what it does. circus-0.12.1/docs/source/for-ops/cli.rst000066400000000000000000000025411256046442300202110ustar00rootroot00000000000000.. _cli: CLI tools ######### circus-top ========== *circus-top* is a top-like console you can run to watch live your running Circus system. It will display the CPU, Memory usage and socket hits if you have some. Example of output:: ----------------------------------------------------------------------- circusd-stats PID CPU (%) MEMORY (%) 14252 0.8 0.4 0.8 (avg) 0.4 (sum) dummy PID CPU (%) MEMORY (%) 14257 78.6 0.1 14256 76.6 0.1 14258 74.3 0.1 14260 71.4 0.1 14259 70.7 0.1 74.32 (avg) 0.5 (sum) ---------------------------------------------------------------------- *circus-top* is a read-only console. If you want to interact with the system, use *circusctl*. circusctl ========= *circusctl* can be used to run any command listed in :ref:`commands` . For example, you can get a list of all the watchers, you can do :: $ circusctl list Besides supporting a handful of options you can also specify the endpoint *circusctl* should use using the ``CIRCUSCTL_ENDPOINT`` environment variable. circus-0.12.1/docs/source/for-ops/commands-intro.rst000066400000000000000000000011371256046442300223740ustar00rootroot00000000000000.. _commands: Commands ######## At the epicenter of circus lives the command systems. *circusctl* is just a zeromq client, and if needed you can drive programmaticaly the Circus system by writing your own zmq client. All messages are JSON mappings. For each command below, we provide a usage example with circusctl but also the input / output zmq messages. .. The actual list of commands is generated by the docs/circus_ext.py file. It will append the list of commands to the content above. Documentation contributors can safely edit the text above this comment when making improvements. circus-0.12.1/docs/source/for-ops/configuration.rst000066400000000000000000000613601256046442300223150ustar00rootroot00000000000000.. _configuration: Configuration ############# Circus can be configured using an ini-style configuration file. Example: .. code-block:: ini [circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 include = \*.more.config.ini umask = 002 [watcher:myprogram] cmd = python args = -u myprogram.py $(circus.wid) $(CIRCUS.ENV.VAR) warmup_delay = 0 numprocesses = 5 # hook hooks.before_start = my.hooks.control_redis # will push in test.log the stream every 300 ms stdout_stream.class = FileStream stdout_stream.filename = test.log # optionally rotate the log file when it reaches 1 gb # and save 5 copied of rotated files stdout_stream.max_bytes = 1073741824 stdout_stream.backup_count = 5 [env:myprogram] PATH = $PATH:/bin CAKE = lie [plugin:statsd] use = circus.plugins.statsd.StatsdEmitter host = localhost port = 8125 sample_rate = 1.0 application_name = example [socket:web] host = localhost port = 8080 circus - single section ======================= **endpoint** The ZMQ socket used to manage Circus via **circusctl**. (default: *tcp://127.0.0.1:5555*) **endpoint_owner** If set to a system username and the endpoint is an ipc socket like *ipc://var/run/circusd.sock*, then ownership of the socket file will be changed to that user at startup. For more details, see :ref:`security`. (default: None) **pubsub_endpoint** The ZMQ PUB/SUB socket receiving publications of events. (default: *tcp://127.0.0.1:5556*) **papa_endpoint** If using :ref:`papa`, you can specify the endpoint, such as *ipc://var/run/circusd.sock*. (default: *tcp://127.0.0.1:20202*) **statsd** If set to True, Circus runs the circusd-stats daemon. (default: False) **stats_endpoint** The ZMQ PUB/SUB socket receiving publications of stats. (default: *tcp://127.0.0.1:5557*) **statsd_close_outputs** If True sends the circusd-stats stdout/stderr to /dev/null. (default: False) **check_delay** The polling interval in seconds for the ZMQ socket. (default: 5) **include** List of config files to include. You can use wildcards (`*`) to include particular schemes for your files. The paths are absolute or relative to the config file. (default: None) **include_dir** List of config directories. All files matching `*.ini` under each directory will be included. The paths are absolute or relative to the config file. (default: None) **stream_backend** Defines the type of backend to use for the streaming. Possible values are **thread** or **gevent**. (default: thread) **warmup_delay** The interval in seconds between two watchers start. Must be an int. (default: 0) **httpd** If set to True, Circus runs the circushttpd daemon. (default: False) **httpd_host** The host ran by the circushttpd daemon. (default: localhost) **httpd_port** The port ran by the circushttpd daemon. (default: 8080) **httpd_close_outputs** If True, sends the circushttpd stdout/stderr to /dev/null. (default: False) **debug** If set to True, all Circus stout/stderr daemons are redirected to circusd stdout/stderr (default: False) **debug_gc** If set to True, circusd outputs additional log info from the garbage collector. This can be useful in tracking down memory leaks. (default: False) **pidfile** The file that must be used to keep the daemon pid. **umask** Value for umask. If not set, circusd will not attempt to modify umask. **loglevel** The loglevel that we want to see (default: INFO) **logoutput** The logoutput file where we want to log (default: ``-`` to log on stdout). You can log to a remote syslog by using the following syntax: ``syslog://host:port?facility`` where host is your syslog server, port is optional and facility is the syslog facility to use. If you wish to log to a local syslog you can use ``syslog:///path/to/syslog/socket?facility`` instead. **loggerconfig** A path to an INI, JSON or YAML file to configure standard Python logging for the Arbiter. The special value "default" uses the builtin logging configuration based on the optional loglevel and logoutput options. **Example YAML Configuration File** .. code-block:: yaml version: 1 disable_existing_loggers: false formatters: simple: format: '%(asctime)s - %(name)s - [%(levelname)s] %(message)s' handlers: logfile: class: logging.FileHandler filename: logoutput.txt level: DEBUG formatter: simple loggers: circus: level: DEBUG handlers: [logfile] propagate: no root: level: DEBUG handlers: [logfile] watcher:NAME - as many sections as you want =========================================== **NAME** The name of the watcher. This name is used in **circusctl** **cmd** The executable program to run. **args** Command-line arguments to pass to the program. You can use the python format syntax here to build the parameters. Environment variables are available, as well as the worker id and the environment variables that you passed, if any, with the "env" parameter. See :ref:`formatting_cmd` for more information on this. **shell** If True, the processes are run in the shell (default: False) **shell_args** Command-line arguments to pass to the shell command when **shell** is True. Works only for \*nix system (default: None) **working_dir** The working dir for the processes (default: None) **uid** The user id or name the command should run with. (The current uid is the default). **gid** The group id or name the command should run with. (The current gid is the default). **copy_env** If set to true, the local environment variables will be copied and passed to the workers when spawning them. (Default: False) **copy_path** If set to true, **sys.path** is passed in the subprocess environ using *PYTHONPATH*. **copy_env** has to be true. (Default: False) **warmup_delay** The delay (in seconds) between running processes. **autostart** If set to false, the watcher will not be started automatically when the arbiter starts. The watcher can be started explicitly (example: `circusctrl start myprogram`). (Default: True) **numprocesses** The number of processes to run for this watcher. **rlimit_LIMIT** Set resource limit LIMIT for the watched processes. The config name should match the RLIMIT_* constants (not case sensitive) listed in the `Python resource module reference `_. For example, the config line 'rlimit_nofile = 500' sets the maximum number of open files to 500. To set a limit value to RLIM_INFINITY, do not set a value, like this config line: 'rlimit_nofile = '. **stderr_stream.class** A fully qualified Python class name that will be instanciated, and will receive the **stderr** stream of all processes in its :func:`__call__` method. Circus provides some stream classes you can use without prefix: - :class:`FileStream`: writes in a file and can do automatic log rotation - :class:`WatchedFileStream`: writes in a file and relies on external log rotation - :class:`TimedRotatingFileStream`: writes in a file and can do rotate at certain timed intervals. - :class:`QueueStream`: write in a memory Queue - :class:`StdoutStream`: writes in the stdout - :class:`FancyStdoutStream`: writes colored output with time prefixes in the stdout **stderr_stream.*** All options starting with *stderr_stream.* other than *class* will be passed the constructor when creating an instance of the class defined in **stderr_stream.class**. **stdout_stream.class** A fully qualified Python class name that will be instanciated, and will receive the **stdout** stream of all processes in its :func:`__call__` method. Circus provides some stream classes you can use without prefix: - :class:`FileStream`: writes in a file and can do automatic log rotation - :class:`WatchedFileStream`: writes in a file and relies on external log rotation - :class:`TimedRotatingFileStream`: writes in a file and can do rotate at certain timed intervals. - :class:`QueueStream`: write in a memory Queue - :class:`StdoutStream`: writes in the stdout - :class:`FancyStdoutStream`: writes colored output with time prefixes in the stdout **stdout_stream.*** All options starting with *stdout_stream.* other than *class* will be passed the constructor when creating an instance of the class defined in **stdout_stream.class**. **close_child_stdout** If set to True, the stdout stream of each process will be sent to /dev/null after the fork. Defaults to False. **close_child_stderr** If set to True, the stderr stream of each process will be sent to /dev/null after the fork. Defaults to False. **send_hup** If True, a process reload will be done by sending the SIGHUP signal. Defaults to False. **stop_signal** The signal to send when stopping the process. Can be specified as a number or a signal name. Signal names are case-insensitive and can include 'SIG' or not. So valid examples include `quit`, `INT`, `SIGTERM` and `3`. Defaults to SIGTERM. **stop_children** When sending the *stop_signal*, send it to the children as well. Defaults to False. **max_retry** The number of times we attempt to start a process, before we abandon and stop the whole watcher. Defaults to 5. Set to -1 to disable max_retry and retry indefinitely. .. _graceful_timeout: **graceful_timeout** The number of seconds to wait for a process to terminate gracefully before killing it. When stopping a process, we first send it a *stop_signal*. A worker may catch this signal to perform clean up operations before exiting. If the worker is still active after graceful_timeout seconds, we send it a SIGKILL signal. It is not possible to catch SIGKILL signals so the worker will stop. Defaults to 30s. **priority** Integer that defines a priority for the watcher. When the Arbiter do some operations on all watchers, it will sort them with this field, from the bigger number to the smallest. Defaults to 0. **singleton** If set to True, this watcher will have at the most one process. Defaults to False. **use_sockets** If set to True, this watcher will be able to access defined sockets via their file descriptors. If False, all parent fds are closed when the child process is forked. Defaults to False. **max_age** If set then the process will be restarted sometime after max_age seconds. This is useful when processes deal with pool of connectors: restarting processes improves the load balancing. Defaults to being disabled. **max_age_variance** If max_age is set then the process will live between max_age and max_age + random(0, max_age_variance) seconds. This avoids restarting all processes for a watcher at once. Defaults to 30 seconds. **on_demand** If set to True, the processes will be started only after the first connection to one of the configured sockets (see below). If a restart is needed, it will be only triggered at the next socket event. **hooks.*** Available hooks: **before_start**, **after_start**, **before_spawn**, **after_spawn**, **before_stop**, **after_stop**, **before_signal**, **after_signal**, **extended_stats** Define callback functions that hook into the watcher startup/shutdown process. If the hook returns **False** and if the hook is one of **before_start**, **before_spawn**, **after_start** or **after_spawn**, the startup will be aborted. If the hook is **before_signal** and returns **False**, then the corresponding signal will not be sent (except SIGKILL which is always sent) Notice that a hook that fails during the stopping process will not abort it. The callback definition can be followed by a boolean flag separated by a comma. When the flag is set to **true**, any error occuring in the hook will be ignored. If set to **false** (the default), the hook will return **False**. More on :ref:`hooks`. **virtualenv** When provided, points to the root of a Virtualenv directory. The watcher will scan the local **site-packages** and loads its content into the execution environment. Must be used with **copy_env** set to True. Defaults to None. **virtualenv_py_ver** Specifies the python version of the virtualenv (e.g "3.3"). It's usefull if circus run with another python version (e.g "2.7") The watcher will scan the local **site-packages** of the specified python version and load its content into the execution environment. Must be used with **virtualenv**. Defaults to None. **respawn** If set to False, the processes handled by a watcher will not be respawned automatically. The processes can be manually respawned with the `start` command. (default: True) **use_papa** Set to true to use the :ref:`papa`. socket:NAME - as many sections as you want ========================================== **host** The host of the socket. Defaults to 'localhost' **port** The port. Defaults to 8080. **family** The socket family. Can be 'AF_UNIX', 'AF_INET' or 'AF_INET6'. Defaults to 'AF_INET'. **type** The socket type. Can be 'SOCK_STREAM', 'SOCK_DGRAM', 'SOCK_RAW', 'SOCK_RDM' or 'SOCK_SEQPACKET'. Defaults to 'SOCK_STREAM'. **interface** When provided a network interface name like 'eth0', binds the socket to that particular device so that only packets received from that particular interface are processed by the socket. This can be used for example to limit which device to bind when binding on IN_ADDR_ANY (0.0.0.0) or IN_ADDR_BROADCAST (255.255.255.255). Note that this only works for some socket types, particularly AF_INET sockets. **path** When provided a path to a file that will be used as a unix socket file. If a path is provided, **family** is forced to AF_UNIX and **host** and **port** are ignored. **umask** When provided, sets the umask that will be used to create an AF_UNIX socket. For example, `umask=000` will produce a socket with permission `777`. **replace** When creating Unix sockets ('AF_UNIX'), an existing file may indicate a problem so the default is to fail. Specify `True` to simply remove the old file if you are sure that the socket is managed only by Circus. **so_reuseport** If set to True and SO_REUSEPORT is available on target platform, circus will create and bind new SO_REUSEPORT socket(s) for every worker it starts which is a user of this socket(s). **use_papa** Set to true to use the :ref:`papa`. Once a socket is created, the *${circus.sockets.NAME}* string can be used in the command (`cmd` or `args`) of a watcher. Circus will replace it by the FD value. The watcher must also have `use_sockets` set to `True` otherwise the socket will have been closed and you will get errors when the watcher tries to use it. Example: .. code-block:: ini [watcher:webworker] cmd = chaussette --fd $(circus.sockets.webapp) chaussette.util.bench_app use_sockets = True [socket:webapp] host = 127.0.0.1 port = 8888 plugin:NAME - as many sections as you want ========================================== **use** The fully qualified name that points to the plugin class. **anything else** Every other key found in the section is passed to the plugin constructor in the **config** mapping. You can use all the watcher options, since a plugin is started like a watcher. Circus comes with a few pre-shipped :ref:`plugins ` but you can also extend them easily by :ref:`developing your own `. env or env[:WATCHERS] - as many sections as you want ==================================================== **anything** The name of an environment variable to assign value to. bash style environment substitutions are supported. for example, append /bin to `PATH` 'PATH = $PATH:/bin' Section responsible for delivering environment variable to run processes. Example: .. code-block:: ini [watcher:worker1] cmd = ping 127.0.0.1 [watcher:worker2] cmd = ping 127.0.0.1 [env] CAKE = lie The variable `CAKE` will propagated to all watchers defined in config file. WATCHERS can be a comma separated list of watcher sections to apply this environment to. if multiple env sections match a watcher, they will be combine in the order they appear in the configuration file. later entries will take precedence. Example: .. code-block:: ini [watcher:worker1] cmd = ping 127.0.0.1 [watcher:worker2] cmd = ping 127.0.0.1 [env:worker1,worker2] PATH = /bin [env:worker1] PATH = $PATH [env:worker2] CAKE = lie `worker1` will be run with PATH = $PATH (expanded from the environment circusd was run in) `worker2` will be run with PATH = /bin and CAKE = lie It's possible to use wildcards as well. Example: .. code-block:: ini [watcher:worker1] cmd = ping 127.0.0.1 [watcher:worker2] cmd = ping 127.0.0.1 [env:worker*] PATH = /bin Both `worker1` and `worker2` will be run with PATH = /bin Using environment variables =========================== When writing your configuration file, you can use environment variables defined in the *env* section or in *os.environ* itself. You just have to use the *circus.env.* prefix. Example: .. code-block:: ini [watcher:worker1] cmd = $(circus.env.shell) [watcher:worker2] baz = $(circus.env.user) bar = $(circus.env.yeah) sup = $(circus.env.oh) [socket:socket1] port = $(circus.env.port) [plugin:plugin1] use = some.path parameter1 = $(circus.env.plugin_param) [env] yeah = boo [env:worker2] oh = ok If a variable is defined in several places, the most specialized value has precedence: a variable defined in *env:XXX* will override a variable defined in *env*, which will override a variable defined in *os.environ*. environment substitutions can be used in any section of the configuration in any section variable. .. _formatting_cmd: Formatting the commands and arguments with dynamic variables ============================================================ As you may have seen, it is possible to pass some information that are computed dynamically when running the processes. Among other things, you can get the worker id (WID) and all the options that are passed to the :class:`Process`. Additionally, it is possible to access the options passed to the :class:`Watcher` which instanciated the process. .. note:: The worker id is different from the process id. It's a unique value, starting at 1, which is only unique for the watcher. For instance, if you want to access some variables that are contained in the environment, you would need to do it with a setting like this:: cmd = "make-me-a-coffee --sugar $(CIRCUS.ENV.SUGAR_AMOUNT)" This works with both `cmd` and `args`. **Important**: - All variables are prefixed with `circus.` - The replacement is case insensitive. Stream configuration ==================== Simple stream class like `QueueStream` and `StdoutStream` don't have specific attributes but some other stream class may have some: FileStream :::::::::: **filename** The file path where log will be written. **time_format** The strftime format that will be used to prefix each time with a timestamp. By default they will be not prefixed. i.e: %Y-%m-%d %H:%M:%S **max_bytes** The max size of the log file before a new file is started. If not provided, the file is not rolled over. **backup_count** The number of log files that will be kept By default backup_count is null. .. note:: Rollover occurs whenever the current log file is nearly max_bytes in length. If backup_count is >= 1, the system will successively create new files with the same pathname as the base file, but with extensions ".1", ".2" etc. appended to it. For example, with a backup_count of 5 and a base file name of "app.log", you would get "app.log", "app.log.1", "app.log.2", ... through to "app.log.5". The file being written to is always "app.log" - when it gets filled up, it is closed and renamed to "app.log.1", and if files "app.log.1", "app.log.2" etc. exist, then they are renamed to "app.log.2", "app.log.3" etc. respectively. Example: .. code-block:: ini [watcher:myprogram] cmd = python -m myapp.server stdout_stream.class = FileStream stdout_stream.filename = test.log stdout_stream.time_format = %Y-%m-%d %H:%M:%S stdout_stream.max_bytes = 1073741824 stdout_stream.backup_count = 5 WatchedFileStream ::::::::::::::::: **filename** The file path where log will be written. **time_format** The strftime format that will be used to prefix each time with a timestamp. By default they will be not prefixed. i.e: %Y-%m-%d %H:%M:%S .. note:: WatchedFileStream relies on an external log rotation tool to ensure that log files don't become too big. The output file will be monitored and if it is ever deleted or moved by the external log rotation tool, then the output file handle will be automatically reloaded. Example: .. code-block:: ini [watcher:myprogram] cmd = python -m myapp.server stdout_stream.class = WatchedFileStream stdout_stream.filename = test.log stdout_stream.time_format = %Y-%m-%d %H:%M:%S TimedRotatingFileStream ::::::::::::::::::::::: **filename** The file path where log will be written. **backup_count** The number of log files that will be kept By default backup_count is null. **time_format** The strftime format that will be used to prefix each time with a timestamp. By default they will be not prefixed. i.e: %Y-%m-%d %H:%M:%S **rotate_when** The type of interval. The list of possible values is below. Note that they are not case sensitive. .. csv-table:: :header: Value, Type of interval :widths: 5, 5 'S', Seconds 'M', Minutes 'H', Hours 'D', Days 'W0'-'W6', Weekday (0=Monday) 'midnight', Roll over at midnight **rotate_interval** The rollover interval. .. note:: TimedRotatingFileStream rotates logfiles at certain timed intervals. Rollover interval is determined by a combination of rotate_when and rotate_interval. Example: .. code-block:: ini [watcher:myprogram] cmd = python -m myapp.server stdout_stream.class = TimedRotatingFileStream stdout_stream.filename = test.log stdout_stream.time_format = %Y-%m-%d %H:%M:%S stdout_stream.utc = True stdout_stream.rotate_when = H stdout_stream.rotate_interval = 1 FancyStdoutStream ::::::::::::::::: **color** The name of an ascii color: - red - green - yellow - blue - magenta - cyan - white **time_format** The strftime format that each line will be prefixed with. Default to: %Y-%m-%d %H:%M:%S Example: .. code-block:: ini [watcher:myprogram] cmd = python -m myapp.server stdout_stream.class = FancyStdoutStream stdout_stream.color = green stdout_stream.time_format = %Y/%m/%d | %H:%M:%S circus-0.12.1/docs/source/for-ops/deployment.rst000066400000000000000000000027041256046442300216230ustar00rootroot00000000000000.. _deployment: Deployment ########## Although the Circus daemon can be managed with the circusd command, it's easier to have it start on boot. If your system supports Upstart, you can create this Upstart script in /etc/init/circus.conf. :: start on filesystem and net-device-up IFACE=lo stop on runlevel [016] respawn exec /usr/local/bin/circusd /etc/circus/circusd.ini This assumes that circusd.ini is located at /etc/circus/circusd.ini. After rebooting, you can control circusd with the service command:: # service circus start/stop/restart If your system supports systemd, you can create this systemd unit file under /etc/systemd/system/circus.service. :: [Unit] Description=Circus process manager After=syslog.target network.target nss-lookup.target [Service] Type=simple ExecReload=/usr/bin/circusctl reload ExecStart=/usr/bin/circusd /etc/circus/circus.ini Restart=always RestartSec=5 [Install] WantedBy=default.target A reboot isn't required if you run the daemon-reload command below:: # systemctl --system daemon-reload Then circus can be managed via:: # systemctl start/stop/status/reload circus Recipes ======= This section will contain recipes to deploy Circus. Until then you can look at Pete's `Puppet recipe `_ or at Remy's `Chef recipe `_ circus-0.12.1/docs/source/for-ops/index.rst000066400000000000000000000022461256046442300205530ustar00rootroot00000000000000.. _forops: Circus for Ops ############## .. warning:: By default, Circus doesn't secure its messages when sending information through ZeroMQ. Before running Circus in a production environment, make sure to read the :ref:`Security` page. The first step to manage a Circus daemon is to write its configuration file. See :ref:`configuration`. If you are deploying a web stack, have a look at :ref:`sockets`. Circus can be deployed using Python 2.6, 2.7, 3.2 or 3.3 - most deployments out there are done in 2.7. To learn how to deploy Circus, check out :ref:`deployment`. To manage a Circus daemon, you should get familiar with the list of :ref:`commands` you can use in **circusctl**. Notice that you can have the same help online when you run **circusctl** as a shell. We also provide **circus-top**, see :ref:`cli` and a nice web dashboard. see :ref:`circushttpd`. Last, to get the most out of Circus, make sure to check out how to use plugins and hooks. See :ref:`plugins` and :ref:`hooks`. Ops documentation index ----------------------- .. toctree:: :maxdepth: 1 configuration commands cli circusweb sockets using-plugins deployment papa circus-0.12.1/docs/source/for-ops/papa.rst000066400000000000000000000140321256046442300203610ustar00rootroot00000000000000.. _papa: Papa Process Kernel ################### One problem common to process managers is that you cannot restart the process manager without restarting all of the processes it manages. This makes it difficult to deploy a new version of Circus or new versions of any of the libraries on which it depends. If you are on a Unix-type system, Circus can use the Papa process kernel. When used, Papa will create a long-lived daemon that will serve as the host for any processes and sockets you create with it. If circus is shutdown, Papa will maintain everything it is hosting. Setup ===== Start by installing the `papa` and `setproctitle` modules:: pip install papa pip install setproctitle The `setproctitle` module is optional. It will be used if present to rename the Papa daemon for `top` and `ps` to something like "papa daemon from circusd". If you do not install the `setproctitle` module, that title will be the command line of the process that launched it. Very confusing. Once Papa is installed, add `use_papa=true` to your critical processes and sockets. Generally you want to house all of the processes of your stack in Papa, and none of the Circus support processes such as the flapping and stats plugins. .. code-block:: ini [circus] loglevel = info [watcher:nginx] cmd = /usr/local/nginx/sbin/nginx -p /Users/scottmax/Source/service-framework/Common/conf/nginx -c /Users/scottmax/Source/service-framework/Common/conf/nginx/nginx.conf warmup_delay = 3 graceful_timeout = 10 max_retry = 5 singleton = true send_hup = true stop_signal = QUIT stdout_stream.class = FileStream stdout_stream.filename = /var/logs/web-server.log stdout_stream.max_bytes = 10000000 stdout_stream.backup_count = 10 stderr_stream.class = FileStream stderr_stream.filename = /var/logs/web-server-error.log stderr_stream.max_bytes = 1000000 stderr_stream.backup_count = 10 active = true use_papa = true [watcher:logger] cmd = /my_service/env/bin/python logger.py run working_dir = /my_service graceful_timeout = 10 singleton = true stop_signal = INT stdout_stream.class = FileStream stdout_stream.filename = /var/logs/logger.log stdout_stream.max_bytes = 10000000 stdout_stream.backup_count = 10 stderr_stream.class = FileStream stderr_stream.filename = /var/logs/logger.log stderr_stream.max_bytes = 1000000 stderr_stream.backup_count = 10 priority = 50 use_papa = true [watcher:web_app] cmd = /my_service/env/bin/uwsgi --ini uwsgi-live.ini --socket fd://$(circus.sockets.web) --stats 127.0.0.1:809$(circus.wid) working_dir = /my_service/web_app graceful_timeout=10 stop_signal = QUIT use_sockets = True stdout_stream.class = FileStream stdout_stream.filename = /var/logs/web_app.log stdout_stream.max_bytes = 10000000 stdout_stream.backup_count = 10 stderr_stream.class = FileStream stderr_stream.filename = /var/logs/web_app.log stderr_stream.max_bytes = 1000000 stderr_stream.backup_count = 10 hooks.after_spawn = examples.uwsgi_lossless_reload.children_started hooks.before_signal = examples.uwsgi_lossless_reload.clean_stop hooks.extended_stats = examples.uwsgi_lossless_reload.extended_stats priority = 40 use_papa = true [socket:web] path = /my_service/sock/uwsgi use_papa = true [plugin:flapping] use = circus.plugins.flapping.Flapping window = 10 priority = 1000 .. note:: If the Papa processes use any sockets, those sockets must also use papa. Design Goal =========== Papa is designed to be very minimalist in features and requirements. It does: * Start and stop sockets * Provide a key/value store * Start processes and return stdout, stderr and the exit code It does not: * Restart processes * Provide a way to stop processes * Provide any information about processes other than whether or not they are still running Papa requires no third-party libraries so it can run on just the standard Python library. It can make use of the `setproctitle` package but that is only used for making the title prettier for `ps` and `top` and is not essential. The functionality has been kept to a minimum so that you should never need to restart the Papa daemon. As much of the functionality has been pushed to the client library as possible. That way you should be able to deploy a new copy of Papa for new client features without needing to restart the Papa daemon. Papa is meant to be a pillar of stability in a changing sea of 3rd party libraries. Operation ========= Most things remain unchanged whether you use Papa or not. You can still start and stop processes. You can still get status and stats for processes. The main thing that changes is that when you do `circusctl quit`, all of the Papa processes are left running. When you start **circusd** back up, those processes are recovered. .. note:: When processes are recovered, `before_start` and `before_spawn` hooks are skipped. Logging ======= While Circus is shut down, Papa will store up to 2M of output per process. Then it will start dumping the oldest data. When you restart Circus, this cached output will be quickly retrieved and sent to the output streams. Papa requires that receipt of output be acknowledged, so you should not lose any output during a shutdown. Not only that, but Papa saves the timestamp of the output. Circus has been enhanced to take advantage of timestamp data if present. So if you are writing the output to log files or somewhere, your timestamps should all be correct. Problems ======== If you use the `incr` or `decr` command to change the process count for a watcher, this will be reset to the level specified in the INI file when **circusd** is restarted. Also, I have experienced problems with the combination of `copy_env` and `virtualenv`. You may note that the INI sample above circumvents this issue with explicit paths. Telnet Interface ================ Papa has a basic command-line interface that you can access through telnet:: telnet localhost 20202 help circus-0.12.1/docs/source/for-ops/sockets.rst000066400000000000000000000062001256046442300211110ustar00rootroot00000000000000.. _sockets: Working with sockets #################### Circus can bind network sockets and manage them as it does for processes. The main idea is that a child process that's created by Circus to run one of the watcher's command can inherit from all the opened file descriptors. That's how Apache or Unicorn works, and many other tools out there. Goal ==== The goal of having sockets managed by Circus is to be able to manage network applications in Circus exactly like other applications. For example, if you use Circus with `Chaussette `_ -- a WGSI server, you can get a very fast web server running and manage *"Web Workers"* in Circus as you would do for any other process. Splitting the socket managment from the network application itself offers a lot of opportunities to scale and manage your stack. Design ====== The gist of the feature is done by binding the socket and start listening to it in **circusd**: .. code-block:: python import socket sock = socket.socket(FAMILY, TYPE) sock.bind((HOST, PORT)) sock.listen(BACKLOG) fd = sock.fileno() Circus then keeps track of all the opened fds, and let the processes it runs as children have access to them if they want. If you create a small Python network script that you intend to run in Circus, it could look like this: .. code-block:: python import socket import sys fd = int(sys.argv[1]) # getting the FD from circus sock = socket.fromfd(fd, FAMILY, TYPE) # dealing with one request at a time while True: conn, addr = sock.accept() request = conn.recv(1024) .. do something .. conn.sendall(response) conn.close() Then Circus could run like this: .. code-block:: ini [circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:dummy] cmd = mycoolscript $(circus.sockets.foo) use_sockets = True warmup_delay = 0 numprocesses = 5 [socket:foo] host = 127.0.0.1 port = 8888 *$(circus.sockets.foo)* will be replaced by the FD value once the socket is created and bound on the 8888 *port*. .. note:: Starting at Circus 0.8 there's an alternate syntax to avoid some conflicts with some config parsers. You can write:: ((circus.sockets.foo)) Real-world example ================== `Chaussette `_ is the perfect Circus companion if you want to run your WSGI application. Once it's installed, running 5 **meinheld** workers can be done by creating a socket and calling the **chaussette** command in a worker, like this: .. code-block:: ini [circus] endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:web] cmd = chaussette --fd $(circus.sockets.web) --backend meinheld mycool.app use_sockets = True numprocesses = 5 [socket:web] host = 0.0.0.0 port = 8000 We did not publish benchmarks yet, but a Web cluster managed by Circus with a Gevent or Meinheld backend is as fast as any pre-fork WSGI server out there. circus-0.12.1/docs/source/for-ops/using-plugins.rst000066400000000000000000000207301256046442300222460ustar00rootroot00000000000000.. _plugins: Using built-in plugins ###################### Circus comes with a few built-in plugins. This section presents these plugins and their configuration options. Statsd ====== **use** set to 'circus.plugins.statsd.StatsdEmitter' **application_name** the name used to identify the bucket prefix to emit the stats to (it will be prefixed with ``circus.`` and suffixed with ``.watcher``) **host** the host to post the statds data to **port** the port the statsd daemon listens on **sample_rate** if you prefer a different sample rate than 1, you can set it here FullStats ========= An extension on the Statsd plugin that is also publishing the process stats. As such it has the same configuration options as Statsd and the following. **use** set to ``circus.plugins.statsd.FullStats`` **loop_rate** the frequency the plugin should ask for the stats in seconds. Default: 60. RedisObserver ============= This services observers a redis process for you, publishes the information to statsd and offers to restart the watcher when it doesn't react in a given timeout. This plugin requires `redis-py `_ to run. It has the same configuration as statsd and adds the following: **use** set to ``circus.plugins.redis_observer.RedisObserver`` **loop_rate** the frequency the plugin should ask for the stats in seconds. Default: 60. **redis_url** the database to check for as a redis url. Default: "redis://localhost:6379/0" **timeout** the timeout in seconds the request can take before it is considered down. Defaults to 5. **restart_on_timeout** the name of the process to restart when the request timed out. No restart triggered when not given. Default: None. HttpObserver ============ This services observers a http process for you by pinging a certain website regularly. Similar to the redis observer it offers to restart the watcher on an error. It requires `tornado `_ to run. It has the same configuration as statsd and adds the following: **use** set to ``circus.plugins.http_observer.HttpObserver`` **loop_rate** the frequency the plugin should ask for the stats in seconds. Default: 60. **check_url** the url to check for. Default: ``http://localhost/`` **timeout** the timeout in seconds the request can take before it is considered down. Defaults to 10. **restart_on_error** the name of the process to restart when the request timed out or returned any other kind of error. No restart triggered when not given. Default: None. ResourceWatcher =============== This services watches the resources of the given process and triggers a restart when they exceed certain limitations too often in a row. It has the same configuration as statsd and adds the following: **use** set to ``circus.plugins.resource_watcher.ResourceWatcher`` **loop_rate** the frequency the plugin should ask for the stats in seconds. Default: 60. **watcher** the watcher this resource watcher should be looking after. (previously called ``service`` but ``service`` is now deprecated) **max_cpu** The maximum cpu one process is allowed to consume (in %). Default: 90 **min_cpu** The minimum cpu one process should consume (in %). Default: None (no minimum) You can set the min_cpu to 0 (zero), in this case if one process consume exactly 0% cpu, it will trigger an exceeded limit. **max_mem** The amount of memory one process of this watcher is allowed to consume. Default: 90. If no unit is specified, the value is in %. Example: 50 If a unit is specified, the value is in bytes. Supported units are B, K, M, G, T, P, E, Z, Y. Example: 250M **min_mem** The minimum memory one process of this watcher should consume. Default: None (no minimum). If no unit is specified, the value is in %. Example: 50 If a unit is specified, the value is in bytes. Supported units are B, K, M, G, T, P, E, Z, Y. Example: 250M **health_threshold** The health is the average of cpu and memory (in %) the watchers processes are allowed to consume (in %). Default: 75 **max_count** How often these limits (each one is counted separately) are allowed to be exceeded before a restart will be triggered. Default: 3 Example: .. code-block:: ini [circus] ; ... [watcher:program] cmd = sleep 120 [plugin:myplugin] use = circus.plugins.resource_watcher.ResourceWatcher watcher = program min_cpu = 10 max_cpu = 70 min_mem = 0 max_mem = 20 Watchdog ======== Plugin that binds an udp socket and wait for watchdog messages. For "watchdoged" processes, the watchdog will kill them if they don't send a heartbeat in a certain period of time materialized by loop_rate * max_count. (circus will automatically restart the missing processes in the watcher) Each monitored process should send udp message at least at the loop_rate. The udp message format is a line of text, decoded using **msg_regex** parameter. The heartbeat message MUST at least contain the pid of the process sending the message. The list of monitored watchers are determined by the parameter **watchers_regex** in the configuration. Configuration parameters: **use** set to ``circus.plugins.watchdog.WatchDog`` **loop_rate** watchdog loop rate in seconds. At each loop, WatchDog will looks for "dead" processes. **watchers_regex** regex for matching watcher names that should be monitored by the watchdog (default: ``.*`` all watchers are monitored) **msg_regex** regex for decoding the received heartbeat message in udp (default: ``^(?P.*);(?P.*)$``) the default format is a simple text message: ``pid;timestamp`` **max_count** max number of passed loop without receiving any heartbeat before restarting process (default: 3) **ip** ip the watchdog will bind on (default: 127.0.0.1) **port** port the watchdog will bind on (default: 1664) Flapping ======== When a worker restarts too often, we say that it is *flapping*. This plugin keeps track of worker restarts and stops the corresponding watcher in case it is flapping. This plugin may be used to automatically stop workers that get constantly restarted because they're not working properly. **use** set to ``circus.plugins.flapping.Flapping`` **attempts** the number of times a process can restart, within **window** seconds, before we consider it flapping (default: 2) **window** the time window in seconds to test for flapping. If the process restarts more than **attempts** times within this time window, we consider it a flapping process. (default: 1) **retry_in** time in seconds to wait until we try to start again a process that has been flapping. (default: 7) **max_retry** the number of times we attempt to start a process that has been flapping, before we abandon and stop the whole watcher. (default: 5) Set to -1 to disable max_retry and retry indefinitely. **active** define if the plugin is active or not (default: True). If the global flag is set to False, the plugin is not started. Options can be overriden in the watcher section using a ``flapping.`` prefix. For instance, here is how you would configure a specific ``max_retry`` value for nginx: .. code-block:: ini [watcher:nginx] cmd = /path/to/nginx flapping.max_retry = 2 [watcher:myscript] cmd = ./my_script.py ; ... other watchers [plugin:flapping] use = circus.plugins.flapping.Flapping max_retry = 5 CommandReloader =============== This plugin will restart watchers when their command file is modified. It works by checking the modification time and the path of the file pointed by the **cmd** option every **loop_rate** seconds. This may be useful while developing worker processes or even for hot code upgrade in production. **use** set to ``circus.plugins.command_reloader.CommandReloader`` **loop_rate** the frequency the plugin should check for modification in seconds. Default: 1. circus-0.12.1/docs/source/for-ops/web-add-watcher.png000066400000000000000000005343741256046442300223720ustar00rootroot00000000000000PNG  IHDRb iCCPICC ProfileHPTY{ MHɱ4Mi$fQDD@pȂAE 2(&Ƞ*ک=_:sλ@>` |g0  skhvc8q$ٜ‘V"§US@9"~T,# "1\0ˑ|d.&aDOf2?-萄sٱ\HDɳ9$&&r6:o"M&3Z56?-1A edSgkFzb,p TgMebA ,rZ$oQ<7rH?%lbCqu[`~(>%-m1e1ɟe9 9&wpRM 1rD Q|E?&_/$d3]E  S`Lq*'#u6y$^&?6:&eLML}]H+ )ЌAܢOb}82P j@=8Nv.@Fk0 >ipB C  "h lh;AePB>hz C;+ɰ4k`:{ÁZ8Nx7\ Wp|߃kx P$,Je\P0TڄCQ-NT/J@}AcT4 mE{,t2z]G/ѓ F001јtL.S9|bXmn`a[Av qv8_5.nFq$ s%FymK4AI!؄LB!(p0J&Jv@bq+BB|B|O"H֤UXR)8i,E#nrBhQ)aTnJUH!,V.&v[8A\SI|xx) -  &rCSTII_DF>1)[*GFEUPYԣ+Qi4C:N:_LLL9,JVK! [({R%JKpZҲOrK8ryrrʷ?U@+)RHW8pEabRۥyKO.}+)+nPQWRRVP)T4,\|^y\bRrAMDK.&UU=UUjjAjZ՞Q=*+454i414{5?iikhjӖfhgi7i?ѡ8$Tuu҃,bn 0\j!Caaᰑ6v74-ۻwc ƏMLLt33e5m60{ko1?ljbEwK+Ke帕UU]G/_X;[obcijs/[CxF۱9ˏ.ScU iGLjlZǗNNqNNog?ظltvEzI=sWsvor9Pb I/+^e}|>+^+xRs%we/e}**U^gP4| t, |$ nR"\l7BBcC;paaaSk_3n~ڌ}%;^|=sLDHDc7/9Ɉd^q2.(j,.z_xCLIDKlY8ϸʸOu3 ! Ĉij\)n| !B!B!B!daОB!B!B!BxOԻzp cyB!B!B!Bٹ%C`= B!B!B!xvt0zP A6?P B!B!B!BP:~g t`pC!B!B!B!t+(=tg LyB!B!B!ByLcv 8B!B!B!B!Q.e/7_j? !B!B!B!dx).7=Ԡ}yy C m4B!B!B!҃u@= 1?qvuЩ׀ [ rB!B!B!Sny o @{'B!B!B!|Pl7 H@ 'vD0 ~oB!B!B!B('fZN[fތz83:=+w\~ߡ7K?7ٯ7~B!B!B!B)PN0z o}onj+w@}L?Iq3|[G!B!B!B!dSڗ r< 0|E7 s}X/7_r+_}7}!B!B!B!dWbo7Kmy >+l{̺r/`Hm'B!B!B!@߶/ @^SK_r ;~o{B!B!B!Ag@W>f[Arq T5\a s o[go7]l!B!B!B!N@^NO 5 4]n?.8_*_z3ݸJ{0!B!B!B!dW&.`ml3o#v\ f\욊Vvd ߤ7 /7_X^▋O!B!B!B!L_NT`?nޛ7;,po2L?)K-I5?"間M'B!B!B!|P)lo7; @<{K'.rgC^H/}9oo/}c瞋&(B!B!B!B!; L?dɒgo./'=nC ׁLmH_+N:׿gLq# 6k m}?֗oY|PWr3mYz^y'3}צ`^ն5~IKL?_!ʹFR??????c/u;ի9oXjZ~q|Z0vt n-˖lضq?я:XtE RH%h*sJz٬fÒ_,P2ЍHŝ#w\q0mQA8NݹHKOy_cܞ>Kv R1qf#1ӊk`nG*s fC2;`YVK)a1]fv.:JGquPCFOOOOOOOOOOOOO#< O#ۿ͘UbbRqfYLFkRoƊ62֛q} ` 6< B+]\/xQxYgvfuՍ>"KcJUEͼbe']r\k+K]NOߛO/_uuY3ӿ^OeyĉGXB@7"Qzs`e2@W#4l6[1 }ܲ>^Y~u IJ,/=y_ 0qqGaqu1 7ƵOgadHZ:?: Ͼ^_^|Lso{qU~@~zَYo,[r>[/R!,}V? hc![6cf}Ї茳~s#llڕ/oߎ ='^ Ao;[ǜpis#O*l{WУ`Ś1~QcXlw1s<|30mّFb62i\t:f1i6,)"H4'fYxkvfGIW\2+b217n])q6Ӑ27n,3?Xz5ZZZ0f??${1,\;k⥗^ƍQ[[=ӦM{´g}6:}]o&,BEEƌ3gp]v̙38OOO|eFO֝ɓFo gBEI.Ab5e+f2֗ ܛK~oݴu6?LYtFr(h~oX_p3,4QƇ~Xm#k2|`!t,>uw?8V^ŋgg'}]L4im|)W^wߍC=ׯǪU_|L&J???FOO{K-cɜ&q2-&yXeG+,Pa sg`=[V,8$|ttgGxXt_Qt\l>n&ܾ=$;Xc 2+yx'f80u}Д{J0} }z,Kxq2wiaz@|Ǥu- )w7|3yyttt/_{W]uo0?T 뢫k}Yu]?HXDҗD"|7x#QWWݷKO??}?]ο ۿ}-V~ )hoY$E7"z{\2f@Vb R7Wrysu+7ۃ|G;mUb+/"8HWB5fLCmu~ɚ:8J@A 1{ApFK{;a#QSW5@ue=$ ϢusH\اoik8Mwx8yYC;1i9zɓ~JpE/Ǖ]Ŗ-[¼-[ƿbʔ)?[/Ƽy~}]- 777㷿-z466ba0oڴ -B.+ǒ%KpbΜ9q5ꫯFCC,VZç?ilڴ ?pcʔ)8묳p=k8`ضoƍíފ/o~eEʓnOOOOOG0 o`|*bk;/XEЏ/ۗ#uշWt**kBEulv!:ީ" IH'PِFE}-upR)@HT Tl В8LՕUJW"T H!I6ʮK䚥eoyH7j4 Ow0(uo9G( [ XsI)_k$QI݁KS43?īٱ&????濫 m)qxbL6 |7oos=ӧOG{{;4ā8<@"@xGpQG.TWWeҥ?>fϞzXd yp , t_W8&Lo7nɓ8N$mGuu5."~폿H&8Sf1sL̟??88umMܵf7^z)lFEE=PydYXnϟ 8=\̝;cڴixqc˖-xqI'a͚5壏Ljjjկ~7tyuY7o^cb֭%777#D&yL2---a~l6<:MS[[^ohkkÔ)S<˗/bqgy&Ǝqơ3{?<eL4),S۶1bd2v?$---mC_3f@SSCllOOOOOL[{|?D/?l-mnl30طz3omT.aCvضQG#Ѐp˂׺9'֦Ns@ O8QCK;+pR* bnƶ3 LS#$\ڣnҐ+_CI#tG0W׫uLT+y7????濾 xꩧ0eʔ#ykllmw)3Y󺮋nvo|ڴid?W_>Or lj؈ 6D%f{}7o~ߡ#F@.8} h8P߽rzlƛ}c9.f[-7_C 䗺r1q} e3oT<ݸGNš'3+Wao*b@u-fq Fѣ;]jFa9'col1MnwE{w"*GeY9ՕⅧV`ω3f wwFnTdjDiҠ?WdnRuп!Kg;/IC+IaC!ljtLiJe>Qd^[1r]ڇ(ο^'ڵPJS.R^z?8CJpw⡇ˆ#f1qDvi,+Ys,xOS\{B*WU$ tI¿)zmjj"L²,|T*+?qc¿G#<A裏FKK  㳟lЧrDmh"\׾㨣ߏ 3g΄NHԧ>_Wꪫ>s{G9rq:06qA-SZɝur|Gaיy쯟pbŊ;uCJu2݅_Wu&@;u&̚duMI:&R1$vl^ ζu0j~|8ɚHCkT_x3qdrXU&ZZ0rxL3{OT-,kk0\i]_^/o˲Ʃe䷜WD}\'-isMY\۟eY_W0Lcuŕ=?????N{|>.xѣGc۶m 1"r.qPWWl6 ˲Nm6oYKlLdhkkC]]2 :;;whjj˜1c8,aYjkkL&ֆ#G#{thnn@cc#ZZZ|CC,|G>G*Buu5ZZZ"4jkk܌zr9tvv5jn⿪ hii_ʸUUUa^ ˡ۶m ">#(rܠ'r[VtYvn\rݾ__p@q OH[dgٳ?  {X.ԇ [s9B.VZn,osAxe }qs+Vȕc^T0y^ ReYAaDIyt,ˊt.TXqmH"ɹd9QH:r`66A: eݡiXqr_(Nk0m:Sϙ3lDłv }~}eD78o-c]T.ߏDWl"@o78H'!Ia6#[*j#O}|ӝ>Y/n0mΏQ~z t$ϾK_;Gݠ-:Z]V}qeq;Pw7 }AWz01:>cZ~˲e" FY;^yłEoޛ/;%~˧3̩RuՍ蝚OGX1<+y^l6+v9^7|>ɗp$ }n"uCrn]V r2η\ttͲOqGwSnwIQ3xliƥ_֕ b|3ou_*X7ݾ^7W^Ru~a%z+<9Sf G*~DҔ 8Nxy H*eYaoo}}҈uC7_:I9H֝#N73Ӌ`g ?2zay???|??48?w;~۾XgNEGC̲, K1S뗋}ŽyS3F" @l} +4 y^PsFZ1f6vyE҉WΩ\KS\Go]fO1J7#H~̺_5"#1f+s?}]t^ <ڥ.CӻͶ&]w:F:Ƅ L&rڐd~N ^\Dt555HHR}Jڧ趫Ml|>L7,o=} XamʹeuZ;m]Jnr̛ -9Yd q"o2PflYʇZߒ^Pd/7q-c> LׯG}}=NRObR[u`|໹ΎYs}[s]lT ذapG#χa6$`۶m?~<1k,yTTTJCz' d@qXtƍ  2sE?l$)0o (נ]:<GȘ>@a =͏lw*NL,M;2e뺨2\I[6>x^GosR뗚Z fYA\^ޚ/]cu@ߊgSO=iu]XÊGA;̙3[oaɨ5=G=zPу&5k>q#> ҵɟj_5)L+#~N_uM,'Hr]̖`ްo)~7/:}) ¿x/X"Dׁz %K Nz ;j.){6]9dBѥ#kںtfzps9Xn̙t: qJAyo<ˡLUUUzD"|>2 9P&OɌ2ʠ*Ǻ/|Lbr!/yYt<@,enO#na E6 V4zK~bۊŗA뇅 8_Aɲao;˅}r9iHx~m ~18/x7q50aMޑfns\8g8dATE?gYVdx\|T* +<#H%xXV r]qQ2 C|ˍ<7H$"y>oDd cMotx]|>}lMO.8ڿёzMr ƷgEv3@o.CKk %//Xs['"|lA5G[1|'\<777mڄzTVVL!a@_q=P \@d2@zIC%M9_,ˊ ^W$HISΥVnD<|<=@'I69[!$A$č$>Ͻ2,Y;F:zt׃+D7dAG}!!Le dsU'1<ߪ'yZO?I(<'}?|Nߘ72xJYJ۔@8K6=8voNKnJo_F{-׿Sn???????????Ƃe.[b׽ȷt/sR[.}"|i$/Xҍx}c|^> 7( o̢G:9{ܻ /_D:FOO*++/7 Z,_yޚ<@!:[os̐~?O}x' 1cpR~ ޿O#,K@d@g}" rz0Au]TTT 4z0׃ 2 $ۀ޿xo=x.e O.d8te`} $rJd_]2`q捏%JHw#} 7: SߘuȍɈ???????????PfތKXj}f@EW@ F~9~// b~'AlXycs)s{qxݨ?Ɣ)S([OGmm-.]yaƍxgJpEGMMMz/ԧ>^x?q' /c! p7xɇlꪫsᓟ$RԠ<|_.B?I#̛ߔ)fd:epN# X*x^aJ=qW4t p͛Y',R#7 Z<āTkke6l8|?Ç_[ɟ.[}c"OAyŎby9\فbȹqAbqOVX,i€̏}<?aOš\zn7~6oկ~6wy1cЀO<T /q1`۶m< ,-|Sg?wL:O=Z[[`p }> ,X^z ')S ,[ O?4 |3/Kr! /} .2wo~X38l7xcz$׮]{bҤIhnnƵ^iӦN}vaap ;xb^?;g}6?p>2{V \;͌e޸kNaۅ'N Á-K. ۅNOr>xe + b`)4@2Pʠ(}Λ8LT*L[ՃΗNKkzopAY^yRPHto9^ާ%ok7zkM&o)/S+O&)q IDATxccu9f0"S ŭ+~@ W {1bO>;ԓ KS)>R)WۆSbD 8~g۵p3'lv5H7NWxn |ghhii `Ip}_|quGOlŋQYY~^{-K/??bݍ*XJ1"g7n\ş'̝;r`޼y s qOS̘1_N; A`xqףϛr .<ø뮻~zXضmn6_ֿFq$uWFmu{:"'O>@deer2uuu!LFs=t7$ ޛ \.r!V_ΡF\s\XV2uʶmrZ] 4=3ț79]jG\SI:ӿM<(>eKHfOOOOOOOOOOO?1~\0?0~m㊥7h3?{R")R4#*ml.gLണ:1rU!p)v^9/v%O?Xd .袢AߩT lf.N0r. ^ɍ죯Eґɗ!} l6̣S۶ݧ |ėy-7r}܄)8I_"G#7{COOOOOOOOOOO=r1gs-fK qgky\'*"Jp]===a }·n U^|>l׿5|Ir9[sF# {…[n6mB[[Z[[NQUU_|@#hooqEn@6իC ~;҂z˗/G|>xmmmxG0rH̜939/iXjUXAK/+W / _d[>GWWlXbѢE8餓fo#eYXf >ѣc;=SeEIe}y([nEss3s˖-hkk֭[.$ ){ qO@l6|e?r\C Iok2bC.3 ЋNK- t$ $mGҗt=/:g>.}%%X"l~u!u oGpI8#uH5sNw VX٤`;2Q@ +ƒF*@*TV8ш#}l޼D oh ~r_Vbk>xmLHZ {/~'8?TQQ|>*yTTT A $d2y, twQYY;::0rHd2x^IjtwwR)(@˱&)r9<voz([1|P7@$aG"@7:P}|)/u],[ \r l_l~@ܿ}v ~ۘ4iFq†;;>Fwq 0׵ؙ3>jpxWqI'<Rpd@”:z6zGp|ҥKq.e98MST}R\ `ߣ OiH0_*G)}H+ g3aۅShٕLٙA$o=8pO“}mG|>+9t29p@rHR72y/1²0ϲ]k<8e0u'<\< Fnvt^ur#lFEEE8K>d uT:fAnӗAysk7@A%-s*#˲kH/O rGKqbs8RޡDl _t0ǔs\93nJhY'@mihwcm`֭ ر##ƍ@Mee%lLYcY@SrU-;% d2Td, H&HR}?hd`TeEFi|ߏCo2dO(@, 2 0fYAHYI_,NC<ȃrr~x`v OAI#7T* xvɣ9'Hn͚|???????????GX/@ *Z?;sj}Xo,ZAbM?oB*mww7^}Uva?>G O?4L2̓ȗPU?(L '@8Ȁp _b<ԩSQUUyM_|2zyNF} e)H8%/<$zPk'}ߏ @x9#ʍ#_AJ~\05PmEX}.Q7 2up*GOOOOOOOOOOOCg^ `JOoNoNoNoN/3>J|#B(]j{Hڿߧ*,Xesh֌+Vq|C?r!F&Afdz<B`[,Xp.ܼ}ܼ ۲ܼ {>?@>=ͻ`a)Xz Z[ZlFIv>GGw<؅"@/pz rc;-0?R>r| ۞'۲{> m '6HFYQ!tdk0>H8 )$DX]ڑ H9N|>PbSqZ%X5 .NAaY{wcaٲeغu+tێ^ OkkW      pH$gܗݴG+/e_+ߓAAAAAAA';+}b8>/N4C        Ow X$U&$n~MPu]GQ'ˏ,/ޒ#? ___Y$///c/|[ͧ%D~XE!fn88"kޢ;QUUŶmHMM38cr5jcƌ[|z;//}5G?rXſGa#9,D|ovb۶m0|rss4(@QQw;_bԨQ%cEG_____ /uÉ___A`x* oذ/+y摙iuzk+/`Æ q$Dqɸ*'v"_/[_[x "ct~{;on}/ > m݆j$NϠAxWXr%.G    ̐!C5j&)hܔ۷Ct:^_"1h aӦMs 7uSEAUU^|Erss ٕD_֖t%[w+X_G_š5kΦ$M$C544qFNʈ#LD8|0w?fܸqDG|ٳj DII &--GN͉'>׋餶{RWWǰa(--5LKgӦML:2233kiiaݬ^O>n=ټy3SLRF+?vVۺ2p[c®]mh3O744t96/w󷶶sN֬Y$N{ڑh_OߑqNLE>$ݻٷo ,Y%iW]u<)PZ[[IX)ɱ^':)))L6UVEM 6:NU 3ì?ڸ}1uTƏ=ꅳ;7nSUU5Yhl.דٳd2*D7'q_u222(((>S>N9RRR59^7!DsXdL2Ձ]]m{Bejjj23`\=TƏBux"3 ۷sc2}d/73LB/THӅ\8LJJ fs. Vkzkz[wX,L>+gh'++OeZ:Yf}HJJJXrem۶1b&Ob׫{_[oi\9rŨKP?A/u]7>QXX%\ºuXv-SLI8O$SWm]L)Ă J`X9ոx^t8?;x+XQYYɄ ?I!tǠA|Ic෿my Dnn.+Vࢋ.2bا;x2^/ϋe-\XMqq1v=nS2qwyh>n=\:;;|h _/]jT`p2 o̵^ o~."***Οl~Uǃ>M8|0YYYQs:---deeuMϱ6毯p}D{bWg HB>`d"55UUill7_/wa1#MkǡC8IMMd2FӽE> c0܌مKyUy%Rͮ]p\}uXf #F ^l&==`<ۍ(l6TUMp8aNSS٘Lns7_{e/^dbܹs9·~kŋ5jF2 ~|ɼvyq$9---w}0n8뮻Nfff'ZĊ0Nrsshll$55EQ(((G=\'wwymcEX&FTUa ذ8ۛN^NSuL,N?1:,耂R;MQ5駟δiXz1~ɒ%\x%K+Hǫ8@iii­gt5RQQAffqB4RnJEEEX;\s׃q{=M'iTWW3l0ݻܹMӌ;JfcLA5>Mu+Vt!qug]9TEG91+\+V3EEE̘1s92v'|ڵkٰav{y0r?".-v?y;n2si=E!"~1 CTTTpgE+/555>|HTg%--"?| ߞ76l@nn.]nVZ͛f7n~ ˙3g=JMhii?rJ:ts2rH… (FSN9EΛ/=_d qW3gc_lfѢE,^?ielg.yCyf/bP.#'nU]w'?ۍN}M ۈKu$|>_R^ɺ D~<0au{`n^z%{=N:$:, GEQ'&[b7NeĈa 2hoxKdػw/477Xn݊i\wu,Z-[0a^/K.e޼y 2]vtRTUvrcZٴik֬aذaak.-[ƝwKdWe]2&Z"剿+_?Q_'&Z"ߴiuuuMl 2bȐ!ݻJ%pcKa[xR/_$VCq^m/;ͬ[Ԯ}qvH0v'}s37֍V.\ȑ#;9餓z455nZTU5n"?tIfTU }c9c0:8\Xǃͷ07tf֬YF#kQ__Ϝ9sb2#FpUW+xg۰lQywޡ[o5쁁7-[ɓlZ nܣ ԯ?箻j|wph'|2 ayyzoeo>Oe9âEػwoE1zM)**bǎNoٲg?ܰxL&C%--1{XN&+POE w:S\\l~qm⹷;h:' ^e1hO%YXf]b$4t4ڰ(:?öt#I|} M3tz^z%c O4iRBUB D~|UwիysΉzH" ##. {-$y$YK^rV^d~mm-#F$\ C/:W^y%O>|rz!vIAAo{kג½ˍ7ӷny_c4}UWt]`ѢEtttOssWx8q"3f۷o ]tsHKKhq}v?]eee~)++d2xعs'cǎbE [VugtO6F'}9_______(//'??͆bb`2M[V ICCcw>٣od뗒>RCu7R:O%5oBٌ8$5 t:R8HG.(G['4%Դ8I{]!rC 2^{sgR\\L{{1>g30`Kx 4Hng-aƎ q>K"?F=+W2ϹM76.1*Lqx8Ȉ]AWt6Og'he \ƃoA.\Hyy6%J2.Il6Gm~~i~h(JEl6x1 K{ƙřX5fEQX,QAwhN;rx/~ ~m.]b[nG/+{{5..ڸ[o}m"?18sY`e׬Y5j---aeiҥK>|&P__|}tt:Q@+W_})S0uTNgw755sbX{7t}}=K.eٌ1UVh"n.aO9 }Y.2ぅ***x99:dggo߾{޿m۶jժVZc(X222N<9˖-c…L4 _4l6򗿤/<|='fc@/0F3x`֭[tmAo~TN'\.Ҹ+;ؽ>P5#]Hsoc휚78wDfB-{PE]9Ưn~`GǧIOg.o'x˙={v~غ^x>sRRRquꩧv3J"?KGV8yPAZZZcCIE~0Ф;!]VbgRKj®nĤI{|$ >~fi j`ax }7ֶu`F$aӧMٳ_uuuݮ3kX*7I|v)*p>|oOQQַG#w/ݽ`압;rE1{aXº埝P+7y睇(Fr fѢE̘1xxru +UUٴi|9úuطo%%%a߿?|[bҥ \tEz({QSSclt:.~-/W^y%?Yx1&SzXxFsnfE4,ƊΨM6]cfv=v|tttOvId̝;ŋىjeܹxB^wwF$hITWhT8C|:&[i@:kXӧ:h (wm6~v?VBk\d l3WUcֆR_Ցv=ȇ͛n7w̜9s Ɯω΀K ˅j!Cx? 333jw&=/nf˄Af.=tKU qo$F賈X,n@ 38\SS)b0ax^jjj:ujt0'|=9Xj5SN9z믿:owٖKii)ӧOgҤIp0uTanvoҥKyWY`CE]6EO x4ɜ'}H.///////srrLJ kRǾ}:t@"?Odt9V{1v d %< ncw؍_N222$c?Y(ָ`:W!-֩뱻u8\.:]..+a##n78N`HQ+Ƽ Ơ֡L|S;԰rMQݯ ,7{(!~ ~Ԡ:tvvv)?(]͛nCИ p޽F#uDj3t`y~)>x-Z{^:;;III1뭷x<\~崶2x`Lݎᠵd\x0c͚5,_BASRZZ+ʕ+9sf9.@@wVm'P رcj2d֯_eG}{ŋK/%B](aٹs'?b`C3`ǎ(B^^MMMƼD^^lqq1f4.jkkioo'//Ƙ.dG~'Of_'3w6)BQQ&MbϞ=]ʋ?&''H f/ϙ3￟SN9L233a%KdɒN]]iiiq@z" /5666-򑭪c r=#L~~>eeex^6KYYF/"b4si]ݶ \$@Yaʢ=~i' 75e5iEsr"&z7RUUd2>^z)iiiƫ8&MĤIp8=:6 "D=Pù:tx#Ç;w.w}7~vQU8xC =2dHnC7!$@T<;}=exÒhX(]vxbyQSS_W~?SQQ3mmmn+ܹsyhkk-l{.¼Ɩ-Grwe)BII _~Z[2t][ajذaܹg,kZIMM [VQȈS@gO2=X_O_ 0s IDAT______kmmEi&_`I2ٽ) '| []7442lXxWIUѱ4,& ~ά*[Ev7x.Ʒi}hۻ#|l߾  /<-'`wqdee-#!6n`9:RZsm?xUZι$zd|g\pfq:[oqزe DQZZZ|8Λ=A?]QQ=O?O?=#<СCUujjj6ԌFd ֳrؿ?7ZC:'4ȃmn`,|Wnn.`Ϟ=b2yՁ:::bNO5xh;>>j3`RILOeM?2D "#E^<:x^<ҲޫGק6 ~_kLGNM~NJ7!hG~^/˗/KgddpRWWw10.DguA}v.҂d;8˭~޽{6mZX, HOOT,!a:@wPZNNNQcVG}n#FpꩧSO0i$?'~VXʕW^ɒ%K+1c_=k׮ [Gw#<7HYYYu'vmc2_(8fΜk.֬Y÷m -og 🢙3g>jMKK ^{ȑ#ټy3.oGNg$z]E.// r/aـwo[VA"Wв'0ύɒBۡM8w0_n3iҩXI?p<"-Hّz'FwL:3ţSRl^.~#{yDY̘MkXV k>|8xWq222>}]QMFss3< -9sK/l2jKx TU%''rLb,SQQSO=ȑ#4i(!CKxYr%sSXXhĕ7 {9rrr?~<3f@UU.\HVV]L6gyݎi\yah nTٱc't֭<vcǎeƍ뤧*aYNUU1ONNohFFFXL~; ,eAFF~h裏-uְ?Sc877˺^{\s5SPP~_7n1ovV+V5̿|;SW\qUZɺ6GXv;<᥂q8\.t=;ݓ!}k[nɳ[Llf W)-&:d;2ܛ|K-Lb|@cłh:h_W+'3ʕ+lưa8묳^HL=DCG(++cݺuL>='N'.K.ӧ.k2Xn&L0bVa[,~ hTvcXHKK3މSʊ:]u,  fޘ`ZY*b<mz0Zi1Yj\@G+b%CK7.@bŗhW~oI<O__?_KK >CD~~>޽;vc4jO[vxqW5lֿzqMwCHtBnZL~)L,{nɔL}xuرc0a|>f3̜9Ǔ+&qufƌpb: {Ϟ=ݎF2uNxX,Pۻm6~?W]uU\'t-M7I r@=_/SY-4vrp?z^/b:~EAEDŽ Gz|tE]AWt&{1l0,XڵkYd IE~,zr y"##GQWWGNNNR݁477c6o/)UUt2cƌ.ES>Ͻ:tvt.0.ZcUhZpqHvxDHɬ/^y/֗Ly"w_{Z~aDOfdWOˏ5SSS|@ >aÆQ]]K,/oXb@î+Ty*0^wm Tڌ{jhmmEUU=\RSSQUMӌ3T@ =z4'b6Q+NMΛμk24Q"ߝ٘ʆ]׉򻉶LOʏ5[+X;ISNSĿƻ7lM?~Qo#:g׮]I$:'VcFsR-ŪUw6%zREXjƍ3DӴWP')4כT:"////_4t54;0d*:^O'{pITVV6[رxf"Xu<Cwy4hvqqs<蠶=8:m,ǣinnfL0+'Q߶<1%!vәP=q ovB-![RWWQU5s(i|dgg'     |QULz_u8vKMMMTJII GVDkk+n(p1l6~<O:XǓ(=+۶<1%9Hz2k֬EHX"D>.mFUUg}6999^@&>#˙8q\t    ' =92Di]߶屎)QαG /O=ѺnFIff&XV&LnG4t]7>~TUJ<gq466 5AAAAAe$~SAv.o$C?Ɣ(_ȏ&;;ٳguV>f3 <" MӨe߾}|>ƏرcinnO       Iw李CR^^NSSTVVVF#''6RCAAAAAAAH"?\.. رc8q"x(FnN^(躎(FqLm|///іIX^ſ?D"zx^z`F5wo?ֈ~Gvyޔ/)X#9_{SF?;rˋcwpo_|MӾAAAAAAA!!ԯ:A'<=kX';=竎W_____________ſ?D_u       "_sAAAAAAAAb@t/       _$/       I       B?B}jRQU:AAAAAAAD@GgX`L&3Du       I">b`OM%#=tC 7UG'       |D~`60T|m[YrV\DVf:khikG4t]Α^7(( b6( z4EQh~d >      0D~`6XLhĜ>,pe4n^фu?GQIzUUUQUIEQUlV+& ɄfA5蚎df tUP` P/uhI  ٌjb5c1[ZT 訊~aZY$~[!RSP3:48]N:.ZZpu54]Kx[       |D~ybNEu4O=F.iCpŠ,nmNN:;x<i@>Th[;MhcuEUPQ@QPLJAu<_`#UŤX,fRlVRRRHYIMa1TL(~/݃qntů{}A$u?ho bG5g`d`s8ap~.Acl#AAAAAAAI~ϯc3g(&tO^O#^vTK6&[it|>wA}߿ϓuW=;3{/ıAP(:RYCdRxa ÊlY ʲdH4I .@.`fv{zxs>|2{vPݿOЋA ÐJnjNGBDXd#A(e~a( ) Ah!Q`i e]|چw}|aOga=k 3N~  J8Aq;cI;1,-- DDDDDDDDDDDDDDn; IRz>pV<^{p4;A1:AX'j"ǂa[;8LHt +-mZ(xM^Xߺp6i딊0  g]HWq&OVYp~hAuCv}>pNWK.!KVQzy7=synf,z""""""""""""""?wHbrd `VB Yޚ>C,2bÈ0bQml:$Z 73CEˋ5a}gtj5v<ťm\$,CKېa?aCX? ]_askkV7Y kpxv.T3ZG+ /.ǷyP)ȿCV$$aeqlkA? XXIOb)Cdq˿ cתNw0s$i)(ۀzң0CFL0[&=aCE=B Fk0y7gtMZ~Su4ɏꏱ{$B9z /"""""""""""""P1v=֙l7><8.3LD!M=#sfyE"%d+M'i6FLL4z4Ϟ J\g* njտįq=7 y ;&?-ټg.ߙwDckkzYo՟`ff* wsٹF*pcW%y2zv!͈OA<Fj!;OY9zgN6'?,eOֽ+.1hr){cKWcsoʻ<\t4TDDDDDDDDDDDDDP5[mW>(Y .^҇ xsMU(4!1vыi ,T˖еh9ӴJgfTtZ5!6,]LwEdJ'a`+&'%&xzD In ub8%%#V LjB{ IDAT5:҆:'w+ n% #ApE~ݼeSɯKHWt/,en ,ȃBAm*"s3瘝[QO~x .ToݞڼB=I=azsy5`^`+I!b`?T ˴/ MrX2| ;ˤaJ)kN',1.b<4xP<;.B0~\srenS ,ՅRP Qc-nк?>%%*0>6*i *Z9W8mDDDDDDDDDDDDDD om,.' Yp|eV4Ob_oՎc qpz6L.tVVI! Z{χE(NZږ֕\^JF1REd +\YP&`.5t6`$h1^x&G"܊#h oHaC! JDVt-vCKj3zKJnT#X =[:y(U+4jԫeYYmՄ `0ԪeF4eV]:^,X\ZZ)31r ^ *aX? ! I>OÇEރ54'} F5Ļk!pjS_1$+V]L+yfRH^_Y%#̻yb# =G!n"R.XZ:H)Q0k >c='[{0VBdػHhxγ=%0} &%l< q~F@X\*R(#0$N2EDDDDDDDDDDDD䁧 6x|^ӝ,PԦdF'}6zR)KmH;wJ> kEfC*nzf8ʵ00lp/y),2,,zF%ʪ#jh/Yƴ)wɺBGsgjђ9(4eZ {cb2?Ry4#4`bKdP,X'fx 'i I ֌"I1YS㱆Ӂy2W/׾s"""""""""""""",`]J.6` c4 +H 7!,vNm'îDðߐt IP-yk;KTe\&sq>a)%X|`׮ЙpC*F5$s!vxQ@q5Ԇ8I3h1xO< =&o)V($2uP4[-G;OHc @h x S0X+WϙAa{MM9CrdYB4xy-vSmT̆L`]6lB|ᓘ00Ybd>@FGFK8o 0`-׮!WJ g=|aAZoꞟl$K3K] $ޯۻa?;7G>@Z߂3g(ޜ$_u$]Gim fOc ׳s-DZt'`0>3ns+1fG""""""""""""a<i <5 oS'/Rٻ(i$}_D~M?-`c9H 7(sW}}Rpk&O EWbn}.d{ x|@0l?[L3ðk@^?y7A.l-{FXkJ+#I=J@kv<0JXV?bn~4Mnyx9$-_mӏӨh9z}Iw [?l6Af;ς&ydn^{?Oߦ>s&os^{MzY^mbK;Ô|c* yka=w /o0>o^C71`zW)>$odžx L8籀 i7rQ(ހ./9G xgCh t׶ 'uffhuz#{T*~V^{U>OP8^~1.MWυK3LMN 8#o'OЁ|t?BD^nkad'{?ĩ?$T#H+(E7IƜ>y_2Ñ'x9{^}D&cmTMM.KZ>Sϑ&}\ έtKKKQԽ=QfIxdh*pCN&v[1yxߖ I 6ݿ~^n<`7L ȇh,xJ-qachx \!݌B)8 m: ~'o:}Ц8k'$Zh(8K0چv/!NI0+?\ a uN(`0;7B|HnK.RkP.8,rbJT(ZdYƅ x'1 o`a}]R'u^Z G=,,.!O>K>|fPAC~bC3mݾ%W`1(, ~[S7ʆ~f3}O֍1ef#S3Ѱ4FBN@RFgQ:JUKJΤxla"fbOl)L{0 [O,-_\pfs}{wS` %&''_X__m29V/Om j0ư{>۷ޓ$)Z X\6w- Bw@f\Bc׎?MNe;y4y4Km̰ }qa#`z(X, gYƙgy'9>_ĮX~~"^w ߿"O?$9NTxo܏ge KG&8q$ƆlR5öRH`BCX0V,R^okI c*oeA1XS*v Z1WAnߑ9O5D]3.Y#`N_U/""""""""""""kNڠXk)?~9'Y^Zduiɝk#_ ^ 4)A~LnIYLX >|s"f.fb ɓ3:>Eځ˖^$21>qd{ ߟe]y{jt2 C汑%gh-z2,ocLDYgnH6 &Q#cl<`l4V ) Q^nZ~φ `L&0U]ާRp >)E] >Fk 0/X`1fg9=H)"rA}d͕+ B =,t-&&'Uǯ} (b04ƧX]SXRm!q] Z[? ܾ_$V^?fyhёIxן#ZKxצ2'J,.|ɘMgQ!cjņ\j }fy}cmr)Z._km%%lXńULXÆLPc|~ 4u8e$I]>$Ndm`)DAXKT( JuOX>ޥTJ\0f0X<~?2xG9.X945XZ^%Iһy@ Apm<0wA@ɲщlG7ž{MOW)IJףZ.16:hJAT&Yƹ<8XhLgzMa#{#-mZV\Y^fQR2<"APƄ LTF lP[!I=Y')qG?b,%IYǹ_oZ ?J ʥ"%,ڀ(4x!s~2Od 6ǀf\F֝-Lq;.*g~qYEDDDDDDDDDDDD6( BLXֽ !ߋcV[mz>{vĸM:wPx\8jZHs1Ú>IR)x\fa[F [ s$MI^Gӣ1Ie0;MȲ|bBQVpxaT16$s8u1*nNO$*{Ga@\$ [=Ggtq5eKP0laàOۘ*{tz}mDHG.L:A=A1cVR8o'R%2oӌnOBۣj?]5XcQ2M)=%ڣ?oPvM< jely7a $br󵟹!]ʌO5h%2ӠX12FR9K׋tWh6t{}$!\&<mw뇹$X^iQ*VܽL֟'[ dtdzcCLX',m'$q5W\2K#S/"""OADDDDn爵݉ȃEA}gna( 65Aii k\77o [„UJ92n8OfdY_{&^ ʣ1Y\piy 6(br~ˀjiriڝ.I<7=_[4W=YW0kO%.EDDDD~Siƥ+4[mOM0ҨSWv]yc)~JߥU96P8bW_\T,P,TJ ph1Bs0!I=2O\Ǥi6""""k*M<7شfE"""Qg%U苈o YX^mjwתjU*"b!{q$iFߧߏt{}ܓW?Lsı#lâXkRB((/`!28!2$#P/"""k~IbJADDDjkm7>cPbhWE 'qK,-b ?L3{_SsY?fo[EDD6r~8xQi"""a3-~n3=* EDDDD= {UQ[>WEDDD>$6}ٰXTo?E|XkyAk:tz """""{DDDDD1[q<_`)"""rOEvn_}."""""|{qڟ̋<e\"""OϼSƥ?ݦ _DDDD~'O166zF$"""a㣜A|!l sy""""022)*W/""""r]U*EDDa1"""""|{z /EDDAp%"""""ro)GnE~).""">Ƚ _DDDDDDDDDDDDDD _DDDD^Rvgٺs\2Ǖ<Gf =? qů{?~7|f._=^~M~㷿|<"jv IDAT;"""""|,:g_}{/}0s|ܛ}W ];g7}plPVni^/)Q7k$""""""""{#^7~?8|,3Wx4={o>z&/-{gn"""""""""w|w_}Zݽs<}ys{G;_eW~_IgU_pPk}'Osyvq2OťW(W$).\Sz}}ӭ5\4}yk9:\iF*^Gg8}6L^ze1^}mJ"<~-񭗿*Sx˯կ}sS'9K]~Xo;\^/S?M97Ko.?ep WSgoy^""""""""Q/""""rc~կIK˫9>O +\±Oa?6O@';5Vհ'۷q99:ҠVj27OS,8~pk=rlC۳>=HN45N>Ƿ^~^ϯaUFIkaw:@V'?i Kl&8>>㘯oݿgݷ^0߻{'|c\8.n߃(c?S $a7uEDDn-y>x;䋈|HzU?/~9~w~?/ iZ|(K/S*س{ze)O}?/+75ֽ{vrI>Yppwή O> 9*WٵO<ϓܴ @PLZ|ģ^`)~N _DQȏItyQWٴr7<-s|R3O> =;oKi,˷3:{~O=ݷ5sIGcc?n__ލZv)pju4~~{Bztpyqiy|q2IO8t`?K1;7#o""""""""pSEMrjpxc-Fcִ"""r뎟8MO?~A؉ӴZmgCťn= { ZzCog_d||~ǟ#oަشJscezG URY۝pyc7~7~o/-qEDDDDDDD &R(J{(e{(?hZ4=j|9[raEp}'7uI mO۷g7RnǙsɜC<ݿ3.rq>{oik}/l oe}l&~ vlbϮ׼wrblSu|4(ȿw2ĿQ7uWX,j|[vJ۷]dY'ikUo15Iw|ݻv֘5WOp?O7_? {wdl4s8uzcO o~;z5^?4QEt{8yk߳f&Z7+׼Ⱦ=ۼ{/\>o֣R#2ؾEDDDDDDDᦊ۴1h[^[w=wȏ捷. ڏ=,ϽH|(/-*ftΕ-Oo8q,-7~jb>/ gsp{xz5sr܅-_K2g]ߏV*|8?įoɾU,-/351_P)"GkٻjJs{'oy""""""""pSE-^f*o 諏q{^uwEDDG7;7υK3ܾm˶?4V?A_ l}?w>ϣ#~1?~Eki;9vGVڹ^Tؿw?5lC_"BD=~_}c?BpёO6>J9|e,159ARr{ K,Ш՘' m`8=Ce$&';3?Ljwerb\;:v Xk7.o~~闁#@V_m U~:lx~#Zߢ?hNNno/"""bZ߈11&nNX&'ƙl`nW*9t;0k ݿ[DDDDDDDD.j/""""""""""""""Q/""""""""""""""Q/""""""""""""""Q/""""""""""""""Q/""""""""""""""Q/""""""""""""""Q ?{o{u211@m:СCߡ䃂W~Ǿx>hsq szIt8vk6+xyfff~@E| _Z3==˗i4Eۧl6+áQuwwoWcc$7O,K w(ֿJ-[8?S;Ԡ|B$Zpޥd3٫8Z[.,,h||\,á$=TWWW> .V>|XijÆ ڽ{"^zQ*R&P>nYTZZ4p.ҒGQ⋚Ԕ*++%INS{Ǽ8Ж-[yf?~\Oɓ'Ғ`6nܨ69rD===(hn۹VZV)uf35PP>>1uy ??w.?>1ɩi%Si9U -駷P˄P[]R$)itl\ l6*CAUWUS2iyZYxG'TZVZ\ɩi͆H$Z*/~}2g59=T*%O[ udXy*@Noo\.~РT*H$OLLt#b鴚$):~%R[ (IdRil*//4044v=zTi4M766&0d/TVV*(H(N+(*J) d2 u!oYY>@JKݚ_(;s9fsJr:욟_юN/gf6#ea˹&dn}`:ٰ,Y[(OH:;AI N((*HȲ,>Sf2횚O6R,ĪX=_|EI۫*%IY$xX"o/|>nitpY'NP&JJJzeR~ӟ*4M\.e|H$t:v/9@ʭ?\|n[fI~+yS*ڡwuhlbRrv64۱,KЈ6ԭ+cm+Nvԭw|[6yJ%I:ݣ陰BrRiu:PP;Z0 E1ymj|;uܸ!4?#Uϥry )JLTJTJ *+[zoכ_UpF###PCCv411IvHeaz't:s53LA_'>ɬ*eYx<[v.M~xn^'&vܧPPc@~&Q$цgY%Iɒ> ]ϙٰɔ֯縺B]{D +<7T*uJs< _aB.Cah|bRJ%@>V_.K>O>`<^eYP}}N8t:mIy˥:ԩSr:VRSS^Yt:IRCCCR.>22%+ʔL&5==!á:utt(jkkS&?|DnhbbB'OfS(R** %IyMJT]U;*Lɲ` w˲tOCK&\J4$>N*[U/IXRyJvvnQgo!jjW߷j㒒ɤeY֒ŻnUUUiffFjnn߯hffF?uw-I;3/+ʯܯQIIneoee<ѨJKKY***Ԥ]R.6lP__fggDRnb޽{u}PnsiixUn{U{{FGGuС|֭[ں6psq:yO$$Umfi #RKVW4Mukpx|U엑M" 2εѺu|,.M"`@#T;Ǵ˚\p+!Kr:'?x`?ɤ" Ðɯ=]wݥ;C J*--Z1;n_q={nS$Q:VIII>t:#(+LrrIMx'H$Fe+B|ɂ2˥Ol6vp Ј|}MTMuF&.)Q0P9[XaP^6y2.M?15+={LxN5UWԆiZF^] Cn |Hʥ6 #zj]ӹ$x!4]v]~u\.JJJdYVkaFApJ61],"PO߀&msK5U2M翴*xTn/eŹ}Oo6ԯML9"Pwɤ*+BU(ظ,RueHNShTr\+'S)MLL-flXe]`-#\[Lm@5Pz_ns勏/~sLypؕdU]UQp-̣B@|}msVpn?-qCz5:>)I(Lm۶L{t_)u:/I;踤$w]4eYIPMu׭o[ t9W:~ ?Wv@/_뿙w׊ڻksZnM͊'2 /K55+HtnniڶEX\a$eJUK{ s*|4#I~Wɤ,˒]r G 2W9Z|bRf\?5'@l/QT u øbJݗ׆i8K# X^ KVkW{W='ƲeZJ|˵{9f[(^x~\?V_3W{{us׳?fŠ|+JJ-'E@>_5)(}^Fpc#xOffotpS{ۦFœI~Oe?#fPN)P蘾K JeT_OX IDATW-~9T*BIB$gc{G==ky+W4M%#ppr8g'es~˿*_^zEkdt\_S/ɤ~~}ׯ%I|gNg~ zw p"477h4Z7%4= \rOO=Q?< P*B$w?{34MVWIv=ڼ)~Q419}/oSZݷtӪ[_{ Ji5kMڳMOy57nȗg2}Ny-D.Fi[DۭD2irjZ Ήn.WGD>P 7d:=2GG/d d7$n'w9UWV\ ("n?K*u޷m٨?ݼI45k+ydl\ {URRI몴ְl5@>k|!XC؋=n$`=p >k|֐["~8.-6-" G=ಐZ5@>k-ZmffC@eɲl6>w ?LoK*pp BlCP (/^pmZ5Rwww=(*\ń >k |!XCܰdpU\&؄i6pxN5U?P@GuG鑇Z%I{W$S\O<ڶm^ڎLܹ[sG~TS3T<^/'uC  JRFK3"HAȮ.A@@LF:tTQQ;77y( ӣ̉br:-r֥F"ѨguuR4Sffzd.o%IK'?*ףgc=/?ԿSwOGD76XcWSז*-uR_SrG_\Nv?8~]45=w#mM?]^O,pfgg544m۶Q~_`PmmmTWW2L~2TOOOA;DB###r:JRn \+7=$}gR}GussܻKy0_4MVWJu,M6+*aHըR$K#c㒤2GU!IwR"gg׫Ot[WGOUڹc[>̈́7 nUUU-9ب(W'ImڴIOt_>On[t^k#P$/"\r|jjFa._P`B9n%n6.%ַL\4/"ӡL6+Iʤ)I2g3̆ _ntרt:#G]Z~aaa{՗0 E"AiY%ǣu֩_`PTJڱcfff RKT8^UV<:|줚7,9yeYF'TF4<:2Ol6ے Y:/Ðk)ct:?>i׎풤/>|,˒a,Aj50 -s/ Ðaʞ]rnP}}fgg!y<A%JJJTQQkp><~jpxN#cTMU^~ ?8jζk>.˲.];wl‚^~ bqY7VQ6M6p~hnnn٬<$Sv\.UUUOڴiS؅|ǣ_pܱMlVϽeەN=wyĻ>??eYٶ]ON:V$)(s8r:\E>W>wc޲<\F`o_ZnopmmmUggg>Xrڪ|L�+%-4fdqԫ˧\]נz/)z LTsseٖ]-_]] ڿۯ W@M0 b!XC؋=n$`=p >k|RubYј,*pיeY<ܱl6[!hf6,4e C2sۅ\?ap)-Ppdf6,!+$! j gقmb mőf577Y"IpD"Q!o >X54U^P4+ppM J{)u+J{Nu`Y+3MSe{N*` {C8.-6뤻Cn -" G=d0f)XC` !B ,J)Ju h<L&w6U4S6k]Q?_pIeYzG?ZC {eYWw5hG=ս|n~AR2YU{Plcڷ^}}^{c>%7I׿ɩ;uW_)\Q?+"Rvtjz:ݣwj[Oxn^o$ibrZ×ޕ\bX,ѱ \N5n˗g2-D<pˋ㺭u$CXL}C{wf{ TWVnkxdL$45=s:>`=m C r$IJ&SPnm^9@ ְ2ON鑷̣P0lT* %I9vUTU)K;5=h,T*S5U㚚 )([Vf,ĔٌBrk~p% TeE( k55=ʐ"ѨffH$eT};3;Ym6UVvO$419x<.ۭ*lV3 pB.3ܼ&gdY}͵7X,.wIjk OL^ Wf3Zãm\Ð?GO48<^Hh\Z|@e$?޾A-DկySoծ_y*x7Նzvy/Ddrm1 ` q96}Soy6kСw)L'$σoolxNͦ/_"kog ھu>F܋hhdT~OI}ORhVbq=y[%NܻKLĤyV2c7 JG;:uϝ{Tr6X"Դ\.ff:ݫ;o)ϫHT=`/éQu$>i:ӣݷ"(05=GO(+twZmypEνOΦpdH~WtFUrI:ӣ޾A}^ ζ™ٰ9!PE09;x<խQUV4=;3=rۯaUUZ2MC~"߮ʊvYV~}G$&uܩ##zJ:^zgR/0 9~RusRtA_{@__(GO&%IC# >h4_+?'>MO׷k6=\ٷŖ^]+(ׁCGt?|qgzy+O}JDB_ox[?wP{Jgg~_}C~a巵kv}˒TY䴼eǟ$~I>OzSlX/LCߥY dtu_/@~/D-6-Zdƒ:D"jalFF'70{#0Vj!k\scfaxW{w߶xT{vȘBm۶ĥx"q@nUMu$cQE( ˒::OiCzmjiK|L/|k^&"Z%y ')exW':O{ddٶUi?7*I:p=GeRmoG?xo$IٶU^oY~$;UW>O٪;CG; n/R}KJ-M ٰ^.Iʹ|m ~}ti˦y%ҤSgz4>9__+ÑSiւ6ԭ پ '-wΥZ m{zT^txn^yY%48B%.J\N|WYIR,Wbff |Դ~ʫenO15= ˮԌ PC, kd='ڷ#UV]gkXT]Ј?];]43K6kirjZXLvSi+}pp:jݶY-ǺNwpDUAvbq9e+$6u~ߴ)J܂"XNuUfjjʹr[Vva2TI H)uKL|BdR٬|YI+g6Tw=ݣ[6R5/w{?u5ڲ%?j~![ϫu:yZGw귾w^V*Җ-vߖ[7=3d2%O̓_wk/ߵA`\G;:W}MgzcUտPǣ;oߩ2J50;f3dò,On]<-wg=v(KFs/]سSS3zzWWggPG)e.wޮ۶kauQ&U*V,$ܱ]gzu e2≄~55=lR4SMuj*o(<7a}T6h9[=s ܊jD"t:#4ߧg;ufhZ0Yls$thl\sp5"`Y֪cmԬiITafizfV5U{˥PPCr8fdrFWS]S}:uZZ;0PP%gAu锷̣D2)á*>{nC}2q\.m6U0 yJK<{KH$*.îRw ÐR(Xaұq۶l+ӝʗl6ݾk~l<Gwfsӿ[PP_쓫Za_r}RZr~˿^߯w"(I2z}*o{lJ*xVc?H$e2> Ӟm___g_wYKO~qݵwo٨o|Oe{>sgDz,K;۶Gw1<=}F7)P‚FURnSi[#lY; IDATFcr8JSӪ}Ƕ˝v(X^޾A n]O\sp)jFE2=l/ܶ/ڷ/*qNW򕯬XcFGG500h4wV׫yϦ䜜Twwv)t$ 9r$_Kw:ڵkR;x<.˲t:UVV:yg֭|J&ѣ|jllfޮB|q GT{>T#c:ѩw\.'uSJ2MC٬*hݢu=xok}M%Igz56>{]Lt&&s뵱p_|AժqC]AlxNN_A\ mf9/`ϽOLϹk.}^DgW$aw[V^KtZ!KܠJu #ieY%IuFonl/$L*OxJk_Or\d2 ͫR[L\L&ܼ<gsr8b2٬<ut:򷪸C?Kt]'>www\1~ϊts Ðaۋ019]Jrl ҴA>oR>yIRmMvlߢqm*+V˺DkTWWFFFР@ L&a:tHmmmW4ؘ6lPpb~2"Y\l6p8&y^R)ݻ/L&Rزk433Sȟ訶mۖOa$Q$QI qkkek+Uˁ UUH$eɒ̯y˖ܻ4mPKmá];+O$t8d|Z( ոaE^U< t{NJr)z%SI2$ufmؤd*Kev:j۾Et_ M.|㳪P8W 5hvvVCCCjmmUUUU< C]]] r8 _6^QK}.|lߟ?^]]zK###ڲeesz+(֭[ l*--]0+4եV*eݾdahnf[ީ&Wc@33r4n?.J؇Uן= l6WR00e>j MLLvillԁ USSE|;knn)/&wV\ևkяU:ηd4==Rz.tubۻ{Gp_TV|ZRH$`0 9)w?ձ19NsϜ9S~IIJR$Jz{ϫTSSSr\jjjҩSi&|Pnv ˲V\}n ȯ7MS555Qcc Ȉjjj _x#Gھ}***. ԯnQ8VuuTUUP(Nkaa!?nv ǣeqeYy<|ٺu400X,u-9b{՟uV|>o=W 533T*)577f) itt4?IbY``0.9688(.ߟ/+--U FFF v/_˥R566*#jX(‚UVVPSS MNNng [5RPHǏ͛ N522m޼TWWǏ0 -n<W$)(09[lсקgl6~l6kƍ`Pv]CCCjll\U?p=tww{&G jkkSoo:;;d$V̷jIP(P(lKv޽z˥hݺur\g4MMOO:0 Ԩo\![54M577Ix\6MNsa^?|;amذYMcǎe˛ -@g{=a{<!:KҊD3<+|(Oy~Jǰ, X³h\f߼`{mEE>iWUn:SሪCbC3 ʕHdd1 n5e-ydق/ڗucS nQ{W2MSa0\?LVLF.#k_%uHHʞ}^^T']!b@>#pm1aHB 5@>k|!bz?{wfGuWSσ[jI-f1``z 76Mps9qM$v<%31  ԃZi{UћݓzPwnyzjժU@kV:9sIEp\ G:sDD4Hyyr]9H$IIQ,y=iޡ#<݉z}߾޾~-^A"E_־߯`)7;td'zmOk{N[/_~ذFX0=\3+ҭJReFjjV2҆uen;F(-[RX"'SGU*-.$gTJX3zV2VuUV.֛{Vww*Ku]@L%wM-gTXoN+WJ:7b7oV"eutn֙v=/ީD"[]k4$ց|W:t^^[7 [4 bqi?~Wtņ5+3n=G"J--(7ߠ IҮ=ov)#>Iv7v+i7+?7@,9d2믹R7߸^ǕLuګ7d^՟hT^WGT^Z2.$\\.>z-Y׭VV3J%ީs^n=?zӒǞzN_yCН z_R鏞kͶy:U.(ס#T_?PyY^۹GݱKJ-_r:;#:ج?~>9N]#o~ܡH{72MS7l=w\s#=j9}VOi` o?g_|@ao8p(=*,]s/:~?G [T^V*˶$|jk~>ٶycORkW#C^O-s.hCԣOB]Io*JUQVBmky~cL]˭׭VYIJ-YIvܣkޤUViúU:ph\PEYJ pCG5胷mSjڼq^ymא8VWjau ãcAy.^O|q{ڮ=`@EZP.áW^{S[kڤeKktϝf56>Z;nݦ5TR\qӫd2)Ӑ U==z%}>1+-Ij/V/zGu;:dUWeʖ,K١t:=6K9fNm2.utv4M-yyyӣO>|[ZULwܺMEգOB;wW^ݰUNcJm\,Hy6}Is _!ámZP>n z/oo8߯W-7];álVٶL-gpL=I fKYmX{u]%IA.YX<.^ސO^>{ީ|TOzQgOt}7\3.Lv?0dz}0T]Y{G.]lٶX,_::aek/oՑ J-%) &nyYKK¯_UwON5=oiú-NL[$XI'7MS=Om[V_wK-}~x")]eKkƬkѷ2 CEy߫Sz\NjUs|CGvz۶)kLӔm*+)֫6oĉaFk3k?{1G7m8÷Yά`U>4\t:UVu슨̶eYC?/vqC$I2ee?*˲Z'+'=kvL~ǃzz{ &x"X,G'd:ht@i+` ),K}~&ik6kɒ%zMRgsXo4M!0lg/ ~_8硇w$%YG[RYuRY(y{ضUf[negD>Kt$MT(xvSSp410>y>|c3/od]4>O\i b1&yDwzS$XA~X[6mupQeATMT\vz,ҕ*/\,CF#!?ض-Mt_??֭]@O~_@E2M$ɲҊv<6yB9 ?QhTi* jҥ :dRW,mrV0ԁ7j@@]v8P^q"!?G9rDO֢ETPPt:޽[WVq ˲ݭŋ+ *L̙3ڳg֭[r%IIRccl… %)\;Nk׮]m[/W4UCCvܩM6re=snw6ev?===Z|^bb$E=;GU*R /x޺a X(åW~R׭W~ox"F"?E"577kժU*--͔wё#GTPP0$UPP I*++Ӯ]Ң.,SMmׯW2Ԗ-[2 `0":~2~TXX8j]]]TQQQs`l< qyAY]>,CA"jkkTSS;w{̤ax~E`ȟ 2MMMr:#F`Z_]]=ddmjnnVaa3LƂ Р~`zUZRF:sPIIok*,,T*ӧب˗OyDjΝڻwjkkFu1%I-[l;::555@>ODB4bfX\^Y%߰IT}۶Q0\3"O>g? L0?_E@#β,uvEŕJzU\\8~n` 3m ϭnGcGDzr;/@ȟV^'NСCJӒM{j*=6nܨ#GhϞ=m[7:~6өZ8p@2!=zTdVXh|"ёSr\j9>َ.%!'lByn :tBA\.vtTKl[ JuU+kk N~˙vb$ImhԚ9d*%ӥ?:VA~X[m:pJX2::*(+JXIZ\e9=Ec} ǯO֭eY1b+Ieee*+{mӦMJ&zr:WٶfhJIR<ОOښjy.%IH˖T܏1L[CI/*+Pw0 ɤl)UYijVPpʺZ^7vUci\^+ۖ9tT +li$Б:zĜJ/,]Ƕ+ȱض>-4C#:!ϗ뛦9m߻\}%%O(H0SfLqJAnKwq\T]@k.[ήnGsEtʲν1OH{٥.%I˲&w7 ڴVE{y}^hsm24tbD>`FxIRgw|Q8NuFzf3*JtEPPe0?xLߚD2OjA9?7kVߥ{TtR16m[E{u)ý!rr:UT֙eYJCꄃ~Ri5)J+JkoeO6 ,N`i` &ۖ≄?!˩`4>Lx<*.*ԉf9w/O$40uh klSۛQ2Tڲ,%SIuEtY鴭ޫP^q9KUTlM$Ôno.J$3s¤s~`ɤ$rMD")0r9eY|$%D>`F¤Ӗzz{J睙d0MYU7s-Vsi9fWĽZt/ג=q[7]^8դb=U\\[6J:7E=ѣO>HwɴJߧʊmŗenqaHLYqQ:?KG t zզ̱z/l[*KtoUEY? ].?~WWy5 kT<Ђ2IRGWD}QwH{uOTJ ,nW[IZ`PjJG{ ݧm7\'N6eOm|) 許D^}Ms->BY%˲Jλ  0G޻_?}}?}_MZ0(pI2MSe*+-֭ۮSWG]i9%Ǟ_C,(UIq._Z+UpRkAy6]P JmjrNo_GO|Tm0YL&G]LSUZR.۶0q˶'N/I&w t]۶_qC-Y8n0Wt`@٥׿9?*L<#TQ^KOO)H(H㟃arݳ9jŲ%ڲi6_N/_|Yr$q IO0?\s|!$CH08sLT,SSSRTC: yDڪ]~C{zzt!_^n[:uԈ˵pB% ݻWuuu B#9rDJr8|*++SYY ØSu )??_d۶ Ðmےٞg;޹H$&:yDL&FGig~K&bZbŐz~_ҹNGNoo|>dYVA>~l oB92I mo63gLoxpaĽ\z***RII.]Ey\D2h,1Ib͚5k֗z`3HN*۶=%duF`<ٷDȽ'9t bJ$yGc:k3k?{1G7m8÷Yά`U>4Fr0aM>ig:q>w܏HhL0:mRfƐr{/iEaDSIO&q=^ȟRv9vk>|x`07J|LN,b1[:{JKKUQQ '{6mڤx`5ƻ9rD//^m۶Y|YǏIhTUUUZt%'ڟC"Vuuxx+@9'?y%I\.%I-X@7;_9J)*O{|zem۶-SJNx26BE<;&0t:L&u}i֭;@"niiOޱL?xl˖-Zf<O{y}Sҭޚi/{-{U?H$oYGvoG7My^mܸqd01$[mOt:UTT$IRUUU?vqmw#Itwkd /(hӦM%IDBt=gQCCV\;SMMMzŴm6XBtIZtn&ݻWmڱcN:OC{1E"$ϯJ=zT>kr8z2}]`,w!}5 CgΜѳ>ӧOHUSS#02m['OO .TCC֯_M6ebozLӜ#kkkH$g>_~Hݝ;wĉU^^.˲K/I>OpdokkWUY~ٳGC[neYھ}~uI h޽eY[o[Lӧ}vEQtMJRx"0T]]-ITWW'á Hbب_v%{x`񁁁@ 0ٶGJ<G:p$yDx@PHNS^73/,۶_B_~&D>y+;~V.ө3gh޽Ծ$E!wnO𣍜n0vee$i߾}#ye۶N/^LgL~gtjhhȌ0LFGQIҞ={=>Ç%IEEEr\yl8IR<saua8p@|$IK.͌}u:PKKjjjt]w hd\RԷm֪nHpaHWmzrJ-ZHGQCC ի3S׾}n߻w^|E~=}# ^?hIΓM}vرCK,ҥKa۶n&+:z~a} zꫯT<@>cڻwڶ[73h``@+WTkku=5v8ںuz)?<dy"o_=S}c$p zwРh4??у>8-Iڴi^z7pn 2o4{vuW護WU=ZdɈo~^7i'TZZ)H_j% ?O?~Z>Oׯmֵ^zJeeeڸq㈺0x?CRcͬe=Zvf;ʇ/uV=8KNGwʊ¹C]: @eO>X5d3j'KաCt#-Rgg"JJJp8FLӣP(4]]]|z3۶H$400p8|=^b;;񟟟?nb}wŸn[g9cp8T^^ \Rwqvء7xCǏׇ?9pxD]?ю<w]]{0}HWӑX^<Igoo2LϞ=*++ӦM2LWp|xI I"7Zz퉶7x&zTL$-[誫;_05U`n?|thգ%ko'Bۛ`޺DƟNsF4}h1"3ֈFOvt?˲=ϵ`b`^Nɾ~<3=6cD>ytGPhB{d ?蓹l~&3nlv6ك)KN%ZXR克e070oTWWAp:Zxq@n:}*/i:$IV4֧Ƕ٥u)3D"xTWW0<m׫o?@^Pŕ#^0M Êu=}HE c:nx~QaIla XP^}IǺg1R`$.*l< !>/T /]-{#ΏD>Jkg}L`^H>v6@dĐpQ9uya:u DL |Ho)+6C"0kv?hC潈\.N4S>4J09\tضH̏BRN6V{g CU^RP`#:Lm:VR}Q9LS%ŅZYL.'_q.umaN} 4e4FL#Et@xBuzRٖ͹ @өEՕ+a*uFu\94L'Ӗ%SÕ3j WWe9/˶2em] ݲ,Kݽ}Jr\*.W`FR)Iw5B1]Ҧiʲ)y=y%۶ݣ{|˲thS+kk&=s8Lv"^\ͧ59CV}T׎uK]J$*+-eMn&2iŭ/;d{@Sf$uv(Tgd#SEYN4( ʲl2OȲm],g0\"L7\<߹{BUW-q4u}*N*?P0ڶm7ڣh_!yBf Y p9**L[vtɲ,!uAR[۔JJ57 2MS'TT݄}@64~g,- =A"efdI0x E"çٻ84MoދR\Wqk.ί^73sgGT*I( LڈsE fJzr!MD3wo?ani<~{~Ǜ/A><%og#ds=Ʒ?~CO6|;;|0;y9>~Xv wOON!Nmj?n36۩Ϸ!BG٭vG|S|k;<ʣmmCc=r`/j]R>0^|p4;{+ϣ<]|}s~QWlf' ̹оcǵZmnq~;LA>L8\#6o````(RUո~JQ.D,/ ⃴qS"x ffw|UUeٙqSw?EQY^^(ؿoܥ ٙ̎&0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A0A.vqM8p`%cZ& & & & & & & & & & & & & & & & & & & & & & & & & & & & & & `UU5Ƹ )w1KYth4oa.OV~NE1Rw1++ ji3;ԸKA>SRUU,-W^RKiI7^}ie~Ggovs֝p9?jD4A>SrY|^~!Ϝ<ٍ׿~ϜHܸu'KKՊ:y<-$IWVsYZ^I^ϑCsܻw3Sג$ |Çf|;s~jEԶvRE9ؿ/w\k%WnVd~n6OH}mº\~#N733>ubc0@UUYZ^NOYUS'rq)7oΠ?L9y,A0W{ȡs׏^2;ΉcGt}^nݹ~ٙv=LM{6~*nܺf'}V^j;w_T2I?5׾(sApjʲ8p8̅KW`)(DWh_{͉cGr $eWRo3^{~Tt?Nd|}VaA_o_?8 rp88v˻~?/^p8̃'s۬ |Z_͟a>aFn߹M`J,/dvf7_%s3a>δ_Lq֝e_LMM=Q]GVB؟o~|Ņ,>X\A>\:|5EQdye5y7=uX^xL9y㵗s$47ocG$_\?b㼝p9.]s7KYY9ɿ<3j5wOG(O(-ٟD5<_\L?2 j627;Ȯns+$Y_߸} [?:q,Irƭ]ߞξ,ȟj5SEnܼq|OAVdzzj# +IC˖;~H,ҕW9yh 8X] ggI[/.l<$ʨ~pc1k uemwzz*tV{h6<ڦh7_'sܟ=y '%xJfgڹ{o1o|,Zmte8r_E^}<{T.]2_^i4UUyNL;~?Iߗwz=Sf/.m 󵗳0@QZQ2$X W (2noc"Y;V=|(Wo¥{?{DӧN,˜xyO7?? o޾Sӭ$kzj*B^yt,>xuTUfqq)/XLcG?rЁx%IrhvxˑÇr4|ٹ~f5{3 23pXQn/7oNYVYY]ͭwsA:&4g?"OO0ȧ_\HS'8~?7o\z޻j}Fƫr\|%6ͤjyxoj>;w!/^ɹ 39|(z=z>|~>F}#s^.\L^~̭͖nz$IZffgfC6kZy՗ٹ n<_Eoƨ_Wol_;z80qv]j`'|Ņe=?v$Ϟ~El]kmͷkto~cƦַywl?:/ȁqnNFt;I(2==:^? f#maގ, kֹ}*^7LHq(LZn嗪x'QUz^J=g/&(0Iu`6Mϗ;/qݯ6=Wmelzko LMM}(>b]Vk]׿ӚZ- k"oLk.NNj a<7A>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LA>LƸ ݽ{w%c?zw ش " " wY]r*v;s)kUUf0$EVZCǨ^Ѩ?`0̰lm륶˹ZWeAZf '' ;w壏?jV~wnOl4̩ygh~yzϾ8׮?f?iOw ?ӧNws76^*򋟽d'D?$Gzc&I=d=?|`/.9ɍ+i5y /?C Ϧ܅K9ٿ0ٵ[jO?sԉ=r(z=/_ks;z8h {( sylsx9EIFKe_f3==FÇΝ{jZ|p<Mj߾ 0 V2==Zm9Jf=T7ͼt|$jn޺gϜJ^O2#sj6[g~~.G믾$W377G3$I;CdIly$[YXϛٙv.\PCdnv&K{z4# svwO-42#~($o~ ejL;o)[›Ffgٷ0tjzN:MoӌKispmj;7;$tij5h46'ŴәiOo𡃹tݽwFӧr֝|}VjE-UUı#{Q2I' IDAT{RVq'jE=/Yמ;L^/_ʁ2=5'Bn߹n<{s]R>azOzQyC 뵍E<{T>\;Q67;wg v`Bnݾ啍vIr==ЁY+Ҟ~վ}\фIRV\юMOOj'rܼu'7oΥ+_}e] &Itܺ}7|?oI^|n}? dXUփ_6ܯ;uXy[3\ܹ{/7nι R{S_v6׿[j'/>&~grAy+ۓx|f܁7s~4a~oӧ277?".\L{:N]laa>˟壏?˧rw~=^w}+?0^y`0L5ȯ9vǩ2SUUȾW'_llj6̩jfg|',蓍^{Ŝyձ#Ӟj'}Vm;[mǵmw7}~qcoۼwu6sоqߋp$I{z*={~VW;ip8jfV-z ieY^fgo}I>J2LR}6ش`GܪmMUnY{>o[z3^?rh5Y^WZL}q0~?^/|j~O< ܼ};|il杷^̘icGgө׊Φ^}N|/VqAC(``~BKQ\g )YYY//>ml}~fnܼ59c{C𔬬Rx OɝsI.˟=w?C]4nm{I=X|O??KKi4ySUU>?w1˟%I>ܻ^} s`0ȧ_\ȭw2y̩ÇfXI?$~?bfgSJRrʵmdee53$_eee5Gۯvt\r-oJ|yz޻Nk y39zP~Q][KWW;]+Y^\UOȱ==i4;o,|]kZ$Sfx$I{z*EQ^TVVW? 9v/s_vN7'A>yW6fOffv&@~ye5ͯvؿ'n70ELOMSǵܳssʗO]LKWy3kq(jx|Ǿ>o2 Ө׳0?$YZ^LOO%I[_j-?uخ$jٷ0幅̴9x`W^z>tV;ݍY?&+y3O睯#F>|>u2/;~Hܽ7nF37?7v>|kB} 鮵l<;v;r8 N?_[wFu7 0&ǏW78 z~;]G\zmo ;ѵ𡃙KY37;ٜp9Kˣ|)tvRUI?Hd0x 9ERC>[˛|~J^LO_Qo䃳SղB~+/>οRRUUN8$sqvzF^}$ϞN?_?8ZpXմn2sϦ9ZouݮϜV#]CzbwWύN|I]L?dIQUIvrݤs79ֶ ߒ{A'E5LJVo'Ik*Jn?UI$A`Z7]_VWU;e2AA~^f ˪RozQLܿRwR2_&*TI5LKKsdr_m/'$7%e2ac=e`~Ye?w9[U/3()qW~ްja596): F3AM `R; Vw~8 췻q錄ܿg%1 *AaYe79u%Y˽Q0U UNP`^O?3?[ 'z_;o &LTdaofoWwzL|_gXUY]õ ?I{TeUe?+Ae0Y'#E9d `RLOjS^k&Y>d0ò/,մρWF^' ym >Xk{>)omz(=)u%G5LҌj:AGi4(c}MIY;Os0Iz2Av&6Ƹ `Ms693>,SIs9r43;3 z;wүS>N6잕7,ekowmvrUUI8ZkXYXJzBeTh9`*3M?aU^a5 e5:gkK9}²Y$u9 &Is.+~/nDk:ܮdK9^$9О|}ͩW/G3mU@{A`[2uXC֪*˴<b?Z>0?Ro7^~z2AV}u%[?U֗(3m<l:nY?,t`%xX i]pk_Nzh9Zm]|_}=`he00NKA::ᖟ︬[~nap2֢賹6.A>Y_V.LgP3XIKH̰;XӺkkp2j>(Gm43zTGJPH=.òJT|73e}CU%H/w-GmGme-_9e:2i|$2|8A>^elgvݴkϰ3avZH=?zF*Y6~kkErLoq<#L3Z~9UhFfWug[L7jm1NF-|6xr| Zaڌò*fX2ۜ\k_ Fp϶nn/GTVjrfw-EMV8UUGҨײ:uf2ݨ3kgaf}K'q[lnj#  eU3S̭ͮ [ ߬VQkf/=o_UIw0L,3ldTsTkްLwPfQfѬrfگ׋"GgRJ0s6(gҬ3YYYVYYkW=B1 *i52(G]f=ֺe~dVijnnӨ?wgaTvc6TjE-~ۛezezQdYKYKVK-VVL^\V6 2m?\;7U֗#h^ j[,RVUg=߸fWzeZZEޠL߿aU;n,g[e9nMQetw `GTAY:ö3XΠhhQoYJ;aõQO6 ˲l(2vfZA1lFFm-df8|X%kf=ZڲPU?dfmtf0.ZkF-a5>f=zfL(nQp> v WKoէӨ53UogX /I]azQz-zv_][|=׊̴Aoqv_|Q_ S$Fj)cV(F Q+6Z7Hlxo7ikyJoAf@#<-͟F-MlV8 `gfֶL559#?0=3(/QF-Zv>= 7,i-EWup4'lH: sFdی_4׊Qp^7hPH{-(BpW7}6k A6+clL ê\ Ga9Hw2UNJZ?HgZ=Z-F=eUe?Yڌ(Պ6?3Պ|UO(FkiHFjE2jQ7#ܘ_m?jwqeiz߿gK.ںT3=,;-ɒƭQoU] Y@,,ܛ%[~"&&H'yɉyXaܔ4o>|wle‰ B!B!B!H/rNTgA`X>d 3JToۚ8M4{AD> s֯܉j}j}~.O`jtD}U맟Y7DZ?}D&Zlvmewf~lB!B!B܌Bp!tGXN:y;GidHLAʹj03)-Fu纯:jSZu[ý䇘.Jav<1B]~w+(\4>66* Jާhr!R K6xs).Djfn4F 㽮B!B!Bq3 !'"MG`_ϙInޢC2!ZY\hi|us؅Z/+x`j}O;tZBi,R.RͿRd ta~p"t%ݽcL!BiSu=lp![~pٴZQf& !B!B!7$AB|"/ 2Cv"pv⣣#)hKiYk֜ SHmtA~tU :I?k<662k k $ yOGAkh6\c_Q R;EnuWczdZ/߳`Bq]gSAj\/B!B!z$BOr" T{Q 2iI&uFgvH h'7:W?_.G=ȴvȺ~`:Bgz`viz|:kr2/D~8XhXl8^^R!5el wg?imB$2 !B!B!7'AB|"LJj2sz"V7 5Z Sbq`lMA~ 4(6>FD~<ZQ~`6ߴ֧й?i@rQ`2WW:Dg{P'#O4e|䧉pgӨgsxB!B!BA !'"M'|jxZNiFYJ;$,??Z?MNOV.xf"9mq _+E=T}j}&=L3I( IDATwܜ?#}M ӧ{z.{6]?B!B!Bm _!>!Fm BVHOϚ#B S_|"?ߓϻ6M3DXpz.X}j;E`PQ`PtG6:Br:FCv~Rl 10bjg36~[/B!B!B pN''RdFiMuy{HҎ:?2v@xẫ7sL)L.5i}Lid)}"  h/)n8Q^2!R@ RCfNV+ PK>ϴ)Ό>`Z>f/?4>Z_t.:"yfB!B!B!H/,B0O~;ĸ G(|4VXᢣ˝#M3J1&'l(2j!?gy64]~4>Zo| ziz~\5dZӸ>DGgvst1c5!w2;=H]m^ֵ`lC)gX!B!B! uCLSOpX>BvJj} ;"ȼ9"^#CAvv@ki}z W҄V`\XRo\AՊ\+"mlΧ F+FQe>DfCSMaFV+2 N>װIfJѷxnB!B!BܜBqϖ([o" Z_c%7%Z f cF³2&Tm c VBy7Cj j}ni*jխ.{ 'Z>Vv?ƇM4gvN[Matz (> ys<峹 !B!B!C|!gjcd &w^OkǴ9$1FsQȿf?o03Vk'ky\0-Z)ffj^g5֧ivsgv\֐wA~jַpL~nj}o)/iy(lAٴ=,/l7ؖ!B!B!vH/֏1NI2k10.u'/BzMHA~$j}6^_g&]8sӊMk/4٦e91)d~LayA ?MwfEk/o=B!B!B!nBq"Z)J{v;jRsT`~maTv2Rh'w[_# (ܦ``5V)Dy[Տ]wMd>{E2mj"?t.#V+  !B"·~QFulbUB8f>f(sKMsѳv B!B!B !=pGb z}~"Nk ;@&gTm\75pw*Xݥ5e'q\տ(,pn4mw^B`<ӟ.BdY.66ټ"|!B!B![$ABܣ~===S`y DE.  2 3 @愸~0;9ww}lu Gri<7WFE+ ~>v_o TA{5)*_+{I#|7x|a`π@dl(9:O\XlxB!B!B _!Q)/>z(ZS!.P1kdcx xB</;8(t`4cWkjQܨsWFy|_yj !]H.khnZ.Z)F+ΕMGݞ+D],FN6eo gs M"K!B!B! !S1P`KCi,A֗Ji,ܔ=f#'&WJ(Rͩݬs2t2er)3CLMM}"R\"6ȿM:Hs(㥯E!B!B!ħC|!G!FfM_od=ȏ(B0"F]jU' =mbI)&Ӛ;]t]ڥ ~#І0ʦ( wu.HyA R奍E !b0,?PZO`P,>JB!B!B _!I|= >fX=D֜ t*XHg:%kW7Kf!c ^b8ΓQv(!7毘6TnμU&cR w"K/)~S}X"7zs7}+}>ֹRVUWåk+]՚UӆV>[$1-LfZN.D`цgӚK/3h> _!B!B!vg^!Z|yI$@ćUJi ;@+C+\h\cF>9]j|˻t Mz iݜijh (]+/]~vA_빼HL>+~ =7ws.!B!B!O|!'4*,h,5nlbhEf"&˩__ V32[6򟔛qKI s64f}D2􁧦r̪!PMaVV{]ws~"Ei2y{7mTcb*#g綅>g[PXy"q܅6/S\V%!B!B!K|!'i"-Wer!z!Z 8:J1̷P(vO2On=i}9b.i8YD~_?vqP]YTez{ow=iju&znz9j⎪[XZ _3̝NX,-W@?oR\㳽LNR9[?`݉59W#u!:)ȿ.FΣ|ʌQf]Mx\Ǩ(h=^FB!B!oBq6tGǼ9DL܇5J)vw3%tVtu]ҌmJ;:g kȴ | ;X!rxzܤs-+܅TI٪~" N\!B]XՊ¦Jsz|4>lOZ/ݹRm!|p~Vj>>m"]gsEW\B!B!B _!),˄h} VTE t?RTnNZ?t-! ;' 7e?Z?. 3ls*7eL\A>o]4VOVPfi"wRA~*[(f&vHof4ոXT~5h+J:֔][A~?.ӊo˸cuO?uʭB!B!B!ħA|!.PN+8kMWn 1.҄yR >&a8iVG3v.N5?}."7%[.!zwKγ 2́5dF;|' k֞zŠȿ >{ n}}R5:]J>0M.D.T7ɾ;Y?k>=yu k/^B!B!Bi _!Ma᪴ЇE;E)8IJ;y#/Lp{|۴W͘5G(`oSgY:PʧD5!GR`bJnt ynSݽ&o;bzs)g0J1,-k.DU.()p{lDB!B!BA|!&a| |"M [v`8cTn8,wS;j`m3ʷ^6`e|BqTцu*כ+L$Nvѧf@aCi}fvٯs)**i|H& >DN7ȿNev ?)DU&04Z|!B!B!I/mQfW~ZvVqM¢ 5qZijwq)@tϙD|}`v(J)8ͺ}\iLnv+#Ha4/]󑪽K Z5g޳4..So '[˫(&h[yq>ҳ tɦ@!B!B!I/Bv)͍/>:2S!YƘ& Zn"lb N7f.'/iH/ppgf,_ΥP>\x:ӆQ+Uɉ'._"{U: "v@_*̤U n+w!PV\oW(&^rٴټڻ_I- n?B!B!B!Bam,Uܗ~P9ZYr֊EתdcT~;Z߅Ȭ*kZ_S>b`kafZS]PcA4Wĕ1MC{  Q4.]~- 38Wy֧Tco 12moq~A~i:M'Ugzl=̯>][ !B!B!K|!0VS.4TnцAR~p32]czTP 1mVz}.0^?39[CpT_3rw|5̲_;j`oG+ެMץ5dsU4u}[R`( 5̤FvVw 2)>zmg0Rnb޺[ O>\jD-6R!B!B! ! lV טmCC02̶}*J;dP.sπn[a]v^QS`\0afwIz䦼x|Q*B hՠgR/ޏ.(.4|_^kM?]~un0uW& ﻉWtX}.2\w"_+'&cA Fr> ]S-U|6y~"Q;B!B!B !Ć9X~j}k*7C+0?K(F n݅N^b Wey3~9#jW9F6DL~zf¼=b1o'L@I=GX7y6j hC7`"_1 !´jyj,5ϥO<;[1̨k#Poֽ`j}k\Zȍ&Ӛ#1׹ B!B!b#$B kC`̬֏Dq-mV,ڰV>@iGhm'SV̚}41)~})L `P!E;exٙcn12,;@{,)RW4~A;8fEw)5/xdZS^s"_KV.8-L;i5ךz?91?oUDRPt G _!B!B!>U !~72mWNJR#)Lj2WNȯp9q~=PXpa?h~MnkncFgl,ܔi ?2k4f#g/䦤#v'vHaG:fj(.OFۀ#_3N~sϭ>We:wֳ_V竿G\Z~:>~]nNj| \׼`]Cӝk1hCv!\V,hͬus{]'L_uk)jh}v#B!B!B'ABlP#((/Oj|Mm)'ǡJGK\fu=3Crb`wJRqȪFOxͤާv ֙YL:DQ_iCN ) 3FQ33Za;x0aZ~`𼟺.&3),ogG{|p|F|rAi ي諶aZ;vhMqqFn2t FK671-o\lj qr"?}!B!B! Q IDAT%BAO;ա\+j7OA~v~oum}Vgd:MطƇTYBdzҴ(߹0_R\x?0S>:6g~O)0_=[od[| gŷ>R{O,OF_0̶G|Xgqi1H PzϴnLAx;WW6 O{]ɪK'hRq7G[.07gPq`$B!B!Bq$B j|A~M&: ;LSZΧDž}PGךƇf'&/.YsgYs-mh.vy4zqni"?CnJ^lQדhDV)ȏ 3Cv ?DϢ7+Cf7 3^M¤'DH[ %4nwb 4]|qð\)Yi.ݯ0ke\f[R݊^089 ߚe~d"_!B!B!>e !j #W42=nB0Wvm,'FGޢP<~v콁5dF@sD>v̔̚C*w:ȏJ<0-|'/W0D~,siex-2oSc&Oji?~>=.LQckxu=vמ7"mLn"?´vx`zW[֧W+lZSl'A@hō FBqF B!B!B!BA?Q߽"O!~V έdig|{|bUF+Z),LUŜqau'ViE||xHn nJNOMi|4aA=cvV+m#;}.-/%o?2k/Z]Z?D/G#{v#Jkn$%{W/ެUI>DJVD7'^ߺ8̓>V?a@sy` 'K&\BWCkE-B!B!B!6O|!ؐ~:](^.Mg:gePn}eWO!Fy2=0ZQX֊K;X3o,BW+05@_8?]>Z5*hE>k٘8XrsNhrPk?tߩ܌˝odfyy!!9N~}U]{ֵuvd&Mog{rgf!!Z>Fcnx._zw5pl_R˯2wH=;]w:ʾ-åUB!B!B!>M !Ć4>틷f܍_Z}Sf _? z}RtA~M>tGZ>4j}X] 3`G)f  Ń)壵׺X_+y:L缟dZ5ND"o?w_|;y-Qf957\Ji^lQ~YstI#H+ox;>|Ϗ/ qł^/Ri])εG~O_pɠvD|a.+̚7W׺`ԔU/^&B!B!SvV!k]]_P!(5ܔC5sTG1>fӿ5F@VKaAyg>8~f[>9FYnuֳ;NїzAqLG$ɗ)ObUz]s}y`zBvJJDJ;d՘7S>{?xMfJ6 K׼8e71h' C>b^2y54Mw fLGZpTqXvs(>a~%vOjQLkB!B!B!B!.0-{}p"DO rs8{__9mnJh}ìS9“ϣ5u|ͬ,VTke[䦠r3&=4COTǣ/)`);80?h[>,pPc|t2E'2̹C ̚^MOheb;d rg|LW +Yجq)^04n_xMf4>U(0}¦6 9o BxL{^M»/d&7fp~ZmH,a[а]~CJy4OUy4F[v'~S_WE;eR3i>06MdF<|o8/(4O_ [X8Q-N[2)em!B!B!G|!ؐ~ݍ_-)/ ^s"0iCM,F)yv Gn|^恪1mgo]\QBNg27|ɗ_$(҅xjvTS8oPd܉ӟxy`11 -v57hb[`hlkPfo?霧㯱T/ jS6/ް7ż0wx)7H}&O*d4Ņ6 3[ͻ ?H`cl21?0oh.VBïh}l1Y;k60=K)TAQ@[op%NJfJ _?\sA>pSoy7i59/x<'/Prih}@BEkW~|Gڐ~\%@, z[&BJ g >y;qAS>E1Fxm/\e.ύbS7 vʬ9`2kX5'8W<`}B!B!BlBu;өuaۻK~q vWkZu{څӻy7?k4U<>4͙4DžS;23Q71o'a}2B!B!B͒ _!6ri93ҞB fobNʰS>+G+~i ]:/3jiG@i[^Tcȋ6l/8\|`6o;g*"ӊҦ `w:{W<@iFEw2|To,1wx/*XQ5bq1oq3?`T_S \h؛?0?0̶l뛵.нqa6y]Y WU{t|埝#k.Əo~|K|tv o}snE(3kE<rSd%>z~:o< .|_`O\\O<={y7r32d9bq۸@"5<(w5L?8j7zW&*(g\<`+e|ĴC0z6> x3WGaۇ.X B!B!I/~CϾMܤ9ab;<5u|23] i{Ha \n`haS٘o6i8hWxLlT+Vg<GvϮt6`+Q5n¾_~>]V^G|,)(|/xQn1  H߷0C7~'2St2] apRYs+;B ll<XS~  44C_L?AQnVv:KzW[._9OţO5 !B!B! bji}`[JD?7 cx ;Rj2X@߯P!1fYN^r)fxvu? -ބԇqT 1w~)d&M0o.? xygMf+27hx=!M7<<㫝߱;|~s-5xn|'/83my5 oӦٻ9?_'?3} + ԲU<_>g_Q>Lӯ'Dϴ9es>c/r^Nvg[ΟF[ _V GYZ_菸buΗ;od}!ӊ{RJ3w=2o'=OR59[Lʢ -| J-!7it|?'h_>{*B!B!vI/w>/aWӿR9!eXsyhy,?[d116gwpa]wfFo|jfoQiQ#_8Ȍ4>Ff+U'/^3o'Ȭy07rLgNx:/̎uYЇj/b|gۿ<(?cyɟ؛B)gobdkM@Zήs _ϯf[<ޝ5,lcl<ջz{s'W}qӹ';vUuU\cfМs!  OZ2 r,SBWŭYmTuQ䈆3ʸ݊Y$Kas75-_2d79kjl~iW*zyA]}Qeo*vp,ٖrSaJ:YW^A~՞{I mҮkD~_BVeTK $2 vnymCRvwכOPx*zWUŵ\ L#T.j[xN ڡVfZʊE ykOHHYW#{کc͵M9)/lw+NJ2ErX;f٦(R}+?XAZQ5KV<*naNWN|h|IrFh*h ʸ=Mh~*{9OY}zP`Fו5=q20e߳PObHʲ 5nbm\Qd/k2BmU1+U (~{nH/`ĵOT5_x_X jkҼ6)ԛ@nNe_d̖e*=#}7I]+?kٍ_Tŋ+"٦TZ*jZPQ̊Z}$p& T=ԙiJrR= PU/z6 TojJkUۣԘr~%=ױNj TBQ$0dvJi%7hZzZ P"}CUK^8pK/%\dEz^ɨ{BN )dZ6 )0T᜻߲L#i4;/4j~}@6g2*af$dHJ%uK duߑNvC 030R <>Iu%Z^R+CYfKQECk\S:u_jv_@ij?!CXNÙImZ- GeJ酂pUX3M˵'g>W!C 'uVJzToBP{ں~Qa71ܘ R:o%cw~i %uS*TW5|uLVmQ3oT%I+HO(cKcm0a4d(յ{zb}Cs*{2R4ZeAExr(d0 )fP]vRqvBWU&GfwɐH@W5<ɭ!vX{(}W%_C%#sM8]/}--~jj~c4eȵ,P BI3.pnTdP2/?ZlP1UB-J,ւlsB~X$*)|d4T,wv3ׯz*AT rr*U*j%SJ_UWbP1~Iۣ[zr]SFLCZ#6ZUYİʺ=JƲKb1eW=)To%Z2 Wh{jުhCXNqu%>x jeZeIJ(_ T |ETִ5'/+h4;lWWjL_)T}_Q0R?gEZ.kD vh)(%C:_ھeX5Q~{? Tm*~hLmSUO$iɊfRf(nI6lo|r3AH^%/t%E̪F^SN8_Lq5IbVШѯAyaE FUQFud% IDAT{e{˄T̒Y6RP RB`T5?Tي- ՝R.ާ\_ wK:iu@ց?HRJoXW̲\@zBudSW|BlPn]2zj 4*;i oӷ6 5O/źH"I~ȿ| J|ӑNaw%coC <ՆL%CYQQ n7o~dB_ GuIHKi1U~bR!/h(xnDQۻYZ_1[UTJ]}ʺǿQt~(hVV1+R=*JO?ս={-N8*=cËF]J:;M|*W{v凞+P_ԩ^\ 5f0J8 ߗ"j{IZo@;ޑ;+ )-eO}阭\Bɣ}"mS}ci$P.咴Z-k'6#䛶sq)q-SCRǘ R$?55$vO1oMS)wGDʸC0=ɘ,8҅*T ԝȩ;cgDLa >kCHU_J:I&3ۧ>ȸ&rIUSUrLW};Rl\=S+墖-3@.E0TY^K^YFfRԩ2f ϊcN8-+5 R}SmJo'Ԅ4dgrq[qOII]֏wF#=SϻoŐjK_vۦil ]˔:Iݶ_e'S{N 5ՂHadP$ے㖦rʌFfkvIސd'/ՇRuM\)3! NrsCIbVŢ'?drPS)Mp]ܬ4NH}w!V\ϻ'pypcS:A> B@!A t|:A> bw"Z]ϟw #6:.Z]S{8sS8/gqx ˉ t|:A> B@!EJW 9d:fj۶ӻrlNV+L(C-.4U*fT'2 屣^[/_-߾ffz{祰Uԛeݿ3 %)J;e:1XF~, FCg Pٹajz\xpY:sPW$˲ /˲$IW'e3e3o-fTA3:<}L:u(\^Ylۍ6/,sTuSz ۭɫٖhQl6>6򛊢HQU<ҔdBtJ/U**"/X*i {FQ1^ h ĘzrlˈӽhZ/d0؈nݼקRIݿ;?ܼHfm18 @i5vee~#Z7B&0'w[2-yL8gL}ϒ0dY>G=}'n_ 0dYߝV:lw ƍ_Ѳͧo+5"_rl;<_n,v?@3v3|s|5w}a֮>w}oZcgӟt gmރnztZ.w}pTJUm)8mǏy8뺣Eʕ P"{*rkFHÏ@ן-WIp~6>C;ϣ]ˢw^]߿U{zFΌ'QG3 Co98.:MY!A t|:A> bw- #ߔm*J=r")R"P&R&}67dRW.\6sս$)Hx8ZM岲R*WWZeYJ&e2J$'j`fARYtgF~IՉQIқeAp6G!$۷[#y~suuIʕjhoU,ض_8>p>_7FZf3閿iwfstީb&dYnN]k~_8b=ݹ=]-FYc+r>rjGvԎst9 W۶=G$5{ksPsx?wl[汘#l\(={>+I>yË́4\jm#/IjCjP_O$i~ፎq']?NvsU5ݘN}hwq&u~19 -IvKr{{f`ti%)0$U*W [Ea$˲5.})Lh|T<{{tpVWV}n7ϳGw2$~?$u{^In\&_/&u~19 Zaκj_x|s\6+I{ZA_6TkzqݛS{F wԾZ/YoIz ޵Hml`7 S:fAˍN |?Paؼ.:Nht=0S믮mhem[)˲m]UqT*4yu\LK?}!IS׮ TJrjGC_,:Ƿ8ˎGEmE{?ZU5ٶ?\.\a0<FNT<=|T+kmt֔hWeRZieuM[ŒiiyMbؑ1MS7&5}cRAȲކbkZ[h鿸?$qtgzJ}qkGC_,:Ƿ8ˌ6}nqhnZ]gU*UY/?z|VYؒeY"IC-4 0 -ǟ}ԟwHu{=_[bOgݞp3"=~*km}C~y$INy~7oOO|Qgޡ?~:Ƿ8ˈ 4Ըw'Ir8z|}+Y/?\6s# C=yd'+ieu]͂XL'wbV6N8v=EQwE .$y`G%Io b~aQRYh$YIk_سG;;ǩ]? }a(U,5=ƇQ_T4 }齖{*sU2>*I,lIz}v]W=]-qvy}csm6TTFKjAj'iG{:V9\&=vPYkIR,hw&@/f ip4KA\6m3a|5zaQA/JZ[ok{NuW$Ia(JTTfS[jNif|>xuk9s<L{RIuw5lFk0ܫZ\jCGj6۷Cs-m.>N:rXEIҭ-ƨ|suZ2Q|8?;!OZկ]7rE~}(dFvM-^,im= wu=|L+뒤ё!x|FQ[}oGd"!IZ][o.yJ&OO|NZ^?kC<wst9\6ywЙݹ@jU\7ok-YZYmNo;7cu;ە|á$ih_\e@^$T*WYZVVW:w!GOw_3/% Iimv1++bXLz] *J *Y$iz6_GO(WVpEpteu]ÃQQIxp0"DDlFT `>n޴;z{{Z֕?72MC7_۳0 }e)juͫV+Iw:~mB7&ev34xS;%\Q$e3iY%I=mKqlMߘ_}ѓ#RөC"ÐW*0tcjи\NR;MTOWT<*89^:.#}z|5w}a֮>w}oZcgӟt gm޽4z^}=]gv}_lR.94 B@!A p(:.# IDAT;cX?.|:A>"0亱׍03XYg@ SE<ϓh;?yޙN1erY9DjywڦVX,+wtD>Xyg@ϻp$qe)-,*wU(ϻKD66λ 2*՚%37X78 y ڠ+VW6-I91EGm`(jN9|:1Eű 0ϻpQa\t\뤾ivA t|:A> B@!A t|:A>>@w##puwww#cj}:A> B@!A t|:A> B@!A t|:A> B@!A t|:A> B@!A pEQt]16*Z)L+U:3E4=5yv mKs놆-ͩkz5F#CʤSH+*˪뒤T*nlOs2% -.| 7 @ *lضz{\E,Tj7" ((y9p4eY^m\7~Uk5m݇U岙f/5{IZ\^=x\lZ ܘ#0rz~pp P"Wzpy P28IR\9Vj0TOwey<*iNo۶ߝV\߾^?;X`tJlmKDBk7 PL?iW_|ҜjSՕ^~zA㮊ŒfUT[͑;WJ&Hĕ,hz{3&GUb]6>4>2$ٌȑieeMLZei#(>7 I=}1>j.d(yM'Q}==键Nivn^ˍ7_7Sz|VoN[Rg3 P3 \?A>~ ^TՔLmxh@WTwc,k6SWtʸjblͧovdxPCTm[Ӳ4M??{sٌ߾V^WEJ$Gp8|8''D?eJ%&mvqmbq^ywň|Pidywg :i2q eCB@!A t|:A> B@!A t|:A> B@!A t|:A> B@!Ayw82|^ooyw82A t|:A> B@!A t|:A>m+{S%eroq̜Kռ8LK&vV*;ўavRl+o-. lll4MDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDJgvJҍnWt 旴`KwTnydX_PM/xJͲr^Dcx᳗+/@mՅ8;}2|dx|PQ,5+ӵ$Siu^4 ۻDDDDDDDDDyL_DD7ek#~xܟt|G'aox]ׁxԼ*gH(Jp918IJPD<N^l2U9jbfj~+C;|XG0iX' 'mX< {Ůڲ\+}&R ͗.FKD|#m#p|c2Njr=~KW!@%լw`XڅW>ãSlb1[Ndx4MC6śr`hՏ<ַv EG(rѰNNk+e:fjI& """"""m"" wy&O?/cldQѪA5L?u],0kJ%X,Tn^DcH$Si݃d 4+k0$zV힞c#m]Yn^.xf3YVjF4tU-+ $QlqtVUie.:c$njK躎bQ-iPUmʊQ!IvNv얢(4~$IT:]߿d bW)g;s+b $ײaLcTUiMm\EXln:zmdlS IxZڼ,CEH]^4MG"E  o, 59Wedj{jMa2I,#p]4Mgu11>vY0 ٌӓ㘞AD{EQga$a7 v-cZ9p07Tp\(;8wkH;{(j6 $LM@3Hbms\ `pwa Fa ?}`(Uf 3SF􊣓3c# t:7KCx.G]v!csgx$w_CŎBH$r4@XՂtR,c}st](y9S*h5ݎŻs5ds9c{.1ꪛvM*gKP9O6iOl6gW+Kwb״P#ߋ6?aY"Ç-,.aQU{(BȰ-F03Uz-l #lg&073rC~/7214 O3[.(C4Gr/l.} IFqmck;|%,3b$&wj`iytƸF>Zwpa[e@q"l+u?yry*ږP ?\\1]y2SE]NkC{px\^(= Elnχjp9p8ݘbg%H$a60;zFX|mWڋfF8mz]swpʁiBڲ 0$[tfjtDDDDDDDtu }A;/=2F뺎AY{ Bezfۃ Ȑ0 @eTa61;=7#vAu1>6 IErH20"K _(4q{8Atk;]p](K^x$!`}kG;?S.C*u 016MpxtLcP,$p'g!ry#P/+q r^x A(l==,vq|3Cyquqau].y{G(d$IB67nwG]U@>zŌX;u׳x@lGRk(;|b wt8p8»U|38r*\3SyPelcyu߽~ Aj']9+mQ"@ }@ǍGrX,5W>1r;]moask#CxUvW60;=!zi.}(wB}dsyD6G8rwf1EQ`n#JCeʞ,+Ftmc,ųG0LdF?J]. r>nÃ{w.,b1#BQ6~6vj"Jcnf~b^wD& ߅jE*6O΍Mx<`dqh"C,Ykx-O Fq,4M~9L_4Y._0)TM7 +䨪K`6pzh4!WϮBZAn^yEQZM'Zs?vM@9c_#DAiXzKW!"Ew/ԜnqzZ-x|nM"""""""򉈾PPOF,@4G*7,НR~utjtpu*b;t?<*.AT~Scrhلz{p8w\P,ar| Eͼ\ȪklǘÐ\\C0y(Rcs:\D)l6+[0n˲Enoqfp(p|䁑l6cemXV .'΂aD.5UGd . WN!Ջ'F].#Պ( 5-UƑJq# _Dm/u}?b?p vNG`nf KWd:hN:QrVxܮxho:-@V.X4v%T*ULM֌*{L6o~i*&DMzᡚ{-GW(f3\NrŻ{*Ft]:zf $aOGp:XM  LPD(r^h{ LN4~O\U庴Z,mG#Q8kWUbݔ$A;ZmIUjwvdp cmcn ;Ltt J&wX5T7`sg˫-!""""""~w~6062ԐF9) 7iZFpPl`, PU!`‘(;cczr K0_)FZ-0L口,fsMzR7 r#Q$)c bQ El=i[yT:>3Yxjd[ PW5M~G1r3AhFmZ:xjC~m5Icn2?q%~3:jWn2Yunۛ;MB 7O}G7_ IDAT̿~kZQT116R3b"O6=ϻFe0(g-mE2@WŮ(2fӄIx'noםH$SS5[4JI&Ht60byeEQ4P%YL]7OM ͡n8ӓ >/6wj}|l-ǩ8<:4LE"B67h.]V׷œ)<uv, U]uΪ)+v*?vMNF8>9C"Z;ǍaOQi:N\3SǏodq"""""""DD_.L&:v9/wߘCJjpF粢#)r:T*mL4MiENpRMTjZ, $T!^ֵUv)sz^FxcUt2gUTZ9͌A;88:OG]Q*ɰXZ_Im= lnljd{msu;馎ڕ[au+׽io ]בLj:hD2e|J_=CG8cx܈mM;R/k̶K$Σ}6md* MW5Je'q7c[-l~:d*eX< Z. -|V6Ck3zcPuGXFG"}=GXY(f3‘(\Ngܿ$a<ַv@`fgʩ-f3?}m/ > fꦀF<ۥ3ww H64 G'x~r`tdmolx׿m4LWT[$wy0t|Kpr(xh I$Ip;^MM"LJ'l,Oo$ H4_Vϟ G-`{g:>f0W,CPlFI1Lk:pzJ f,uIDDDDDDDm""hw+ OOc~v6wSK@_of&ǧ8<: j] _E+Ug&`۰wa`?6wbnf 3L&}딲.?ޯl-3co(x1G7(Ef'Zſ{1pqW7f׷v),f3^cN(=]l`{@g`?Am(W]j Xخ :T*`eEL_~oa?FF].'~@ӵvS073A|@yUU| [ý6iEQğMV<{|#PΔo_!᰷\ǃ5AhV^E<{|$<' WZN0׋S7Vɔ/ NGzN},#/f5L:}hl.IaٻHODDDDDDD[_yCl\CoSsSGe]lj&vSj||#_(n월%[(@X,kK YV2B[ŶJYL&S)X,As˲E)ol$CUն;c$H܎6kӖTUCXbi86Y)w}ZOS*(XMV*(*lu뀮7T\Vt]GX$o5}*״B(jm4 bѸˣ*Jl6kr6QI 3mNE 1.믞nBPwWu?Y(07 @DDDDDDD[Ph6{(U(U˵&kmzz2z^/z'"DQҼ'\}ݲm0m$Six$El{o k1;)m]>E0ffSm9(:md`25Ӗ$I|e whS* m;QTX,h6RAh] 0@;6\:4MI}!bLSjc{6RD& nx-#˗lc#XoLODDDDDDDDDDt-~zzp L]i~O236% w:4=~ ѵlVLOu^3adADDnt""""""""ߓ_On DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDK$q9s9s9s/er.=/'""""""_A-W?&ź=6U=7U-TֹUmh2 @ϗ.ݐ/` @CZGZ5[k֫u\.Gw3>-@>-@>-@>-@>-@>-@>-@>-@>-@>-@>f]Rt4BF%-<nz& f ?T:Kםh o>t~0e J hT""""""""_H@;5dp؛w`bl⇟0?;cCE4wՂƇG"J_#ͻP(|ɕ;es9vtSƀy䱾f67 Q &JTUQ151 >ѯ@>gVs8⧟162`m _Ut],0R =4T ^Mӱ{cem&WOjӳ 6wqdo^^@8Edn)@˫ ,nˊIn2{ܮyUUiץidYbiNXy0uK$!:ߪ-U U:׷,CL_CQU28 G ;XP,b6_;iDD7AUT0s*ǘmYx b6cf8'q8F,ޝ2X}trk6TM&LM`r|fG'g89 &F0?;Uz:x!:ܰT:ӳ me2Y)<qN0;=qfjg!Jg!};(XGm d0ODo@VxQ:?~,ckW7ͫ ϣP(a6!2AA{ adȇ`{G `z|#Ta61;=7#vAu1>6 IErHd"K _(4c Op\N'NۅbY.Ջ'0IR ַv{p32X^Yc#4 GF(6Jr _= Q GprB.W(}xܸx|c~ ^< ρv3zzۅ|幹݄\eGB|Y%GUU]Ʉwq# c]ś@?f'H${Bls{d -w(o)"Qhzz=M8>mzP_= "lVkM͊b(*TUf9B& @4Najba;Џjq7IxG"BǍwff!""""""'"BCaC<}h #WrGNxT:]5PDCׁã=K%159APe&xۃ =Pb cE. GgsklǘÐ X\C0y(^0t O r5ǧ٬xpo. "Ѻ} fZA<ƈ1ٌM\Dc TX-f/3",8 =ke=u#LrFz\Đkw]tثPSnR|r:r9r9pn䶥LOnMwӻ\Č9CHdD2I`1v݄Iry5z xWFxnf @&DI@/<*{tرib^  -V(\/AcX^Ybi;=᰷ҊFEU[v-D$I\m;W6,.$,6.?v̜@*Ml`j*n H$Ә`qx{G:^xI뀈?DD_@XF 32M+.UZB6VC8E>_a"+1,_|aGjl2A?rhEVAfsr#Q$)c Jz IDATbQ El=;yT:>3YxjҾ^Ng-A(ث~LOM`}s?$]c& -HmKeZ6G_ fs@U 0 ,?~L~y/q׌Ɇu}Q~;r5F0bzã M3T$)dsyUMro!+Ջ'-Sy~_X,(rw,+p-z;.ԣ۝7Ij hM <7F8>bնY;̕;`M&=Ϋ'1ODlldʩh{kn\r\I^?H\Vc~o!JK& A4Ӂ`8Rt*]\n&h c\@9kXj~+唹t\u\>P>9nft؏^G ^=u=꺎RImүaِM4#˥sNg0?1e/pz N]}7_"T|׳m8>=\1(5M'jx|{12|$ NC1&F;u`e}T/=lIX*!ciwl.Bi p4 g!H&SxA`ېJg=th,L6!,-FLL z&qmP2.3$"!""""""a!"k\DD0Wsrj`|tl3 nAE\Dc5/bq(dEQj;9>x"~cKԆ{G -K89;;s9s6A S?ˉx"i[t]N'b$Tc6+6m:Jt"Ix+\u&p0_>EPY0U=VFnүLr[R/)gK0l@_z$ab|߽~ ӁnjDD7b1CutG8>9 `6sWvɘ>"!ҙ,Oϱ@CՍm`j2L6X<#UUu-Bd2Yai`nmvvr:8>(l+!obemD!B4K%7W*ny[;MWN-;;d*m4 'gAҙ)aJ%јd*o3$SiI躎3x4EQbӌn&I&FH& ol6r7h2N4_Vϟ G-`{g:.G:-լ+26/`1Qe :@9 ~L& <~p\DDDDDDD=>\.o}8g.o`sg;sFZkfj!p|ãNEQlm\5}fjv ;{8;(l\f8>> d2/N)r:Cllʆb1C~8.gO@Lv9}mͻ7,.\+@]1;=*ǍA x`6ucL!`11svGA}ob{{{iKWL%jT/f _x;؏ŅyT:\>8*z< ֯/ѧ0:R|v9˧eb.v݊xxvPop3UU1?3v{ s& (XފgݱLH{z{ё!ry8{BQzB]#ſbXwToߦ禪:ﻪ "Mfh>@4 EX#ߧ#_(n월%[(@X,kBK YV2Z[Ŏ|tJ*K@Q_*PUmNXi;&I24_ǯmKb^ ѵ }y(D2)w? AUDAfYM& >>~K:gODDDDDDDDDDtm6ӓc_!b7}]O""""""""""ω" " " " "/]"ߓh4 hX\\\76˹+OSU(@9SH*KnLZ|)_b>֙Lm+bH"J2@hFծ[U#<  ŲXg[z]!=Wm[ҫyfۺDt1` @/={eyԲtkèzmUQ] ES# # # # # # # # # V"¯>P(~u@6}~Is 8:>m~* ~~~3D2ˤ*/c݇׏PK'8lc}s3 @Dg EQfs؍cp.}6v"Wi"{H$ p>i򸼊0;vw@~{=;-]8;@g?, Y;;븼q*ܯi8]ΎuWҙ,≶,v6џzMDv fs]\Bņ,vInx*@2HP;JoWWx xT:ãSH0l6ۭ*..J$01:r躎Se#٬֚bds aH7suBDDDDDDD }vR3r7L!x2X,:!""""""">ét?Co7@57EEQ 2(zPjL4 yWD ^]7Yt*, >qXZ݀멸^/y> "7(j(jn@ԖHEU!bH1]7 itݸQt]Z5עunq ^O ~tM4h~jFAQ`,ֽEڭʦih“3 *s<|Q[(r*4MjqnFS5nm}7ޕ~?*wuV `dh;=\^~VzHwcL qe~3o:w#x2; Wz??8;(PS1FnӉ۷fs~gɲWpr乫:!""""""x R5 sqprLX^yvxzn @qՍ-dKӜJ[v"{؍AU5X$ C4uB6n[;x<1TEEw(|G'HgxQEzq|zH@aډ"݇;&Lp|zms{Q 135Qщ,Ս-ryBfwso$IB8hE* d2YSClD, dقA U?c`/&Ɔ+O&SzD2_}d+z1} Q[^GrX\^U<(fK4e[Z T飧;Gӓu\-F.'f>0 Pͷl.W7xщv'/mnGpzvo_L3 ?tt|^9M.c&Nbf[~2Pq?}s:pf^w׃s][DQG֗iy,"Nc;{DWDX]Brt`tx Kk(bWC-cvID2c]MgJҸJq} ÕZeUʇG'*;Tؙ@qĺY Ã}>ZOMۡ`'g7D;:rEUcsDa 3(//!@&E.W/@e(;}lDۍO#Ʌ"cldG"a؍a$!L$S$ɼ]- vql.W-D].F Ǎ|E)˧H6h $`/t]nt@X0`>'3ScwatdWWqo t4[]U<3o4,N8}<]$:"{u/srzo^?s<ʙIvgD3h{Y+VQBzP?'M116|!"TUE߅G3uNrN#򉈾Si3K$)|9ܮa +H$e eAd ލGFw ؎ax (e :|^tv)#/`h'gP#әlv"{Gw(N1ssy nay]QTǪxvsԢ;OgQݲ,cai籋 ٬2<&.E6ëo2P%[,% <}\%t`ucÃr -}9D14ߴsW; 2lVkQg184iĪ^DI۹~iq_$BS?mfp1=9P0E)Vq1slt .DXtRP)[ꄈ>b XYBo7\2UamzFp. ]r4fj@W'Nbfsp:Pռy?2ԏwKȍ`7zޞ0l6+dŜt&Fv|6[,Y W.iq8(۩t>X]B"T>o d*0_N∽ W7o1׃ޞnHuL3aiWm]G|=-}*(x~̓aF[A- ANg@~(7gL 1 Xnn7BA?Vqrr<ryH0Zn{,"{t>b7 +GŇCAnl#TtD@qF֝qUb XY߄"#u؃=asr=sW7)Kۊ]pX\^C"47,t]E.ǧg( uoJ')Kx\Ap(pov^6帀b*N+05j|m|zz"=6vxi0  ʭFhݖu{>Z4=su ](Jә,&֖w8/qpx>x<3p(Vݷsr<}4%v"{x;pf& +Fw(MבLZ6"۹ pرwpcP ~VQ"klǧwu IDATkΔ󑢨88:/6-NdMD"$x-_( vqiSOss;rPU՜&>Lw]qxtb/;ɔ9{O"2(B*FOwkҷUӺFyک"""""""jѭpzxk7}mV+z*&;DQy"(UU8@.__(RBlDkxp0Z @Ӵ݌U,S 0w\C>TcIGV$rTu$m<8<ׯ!x=fi--"g-ˉl6P9;esulV9=+^4'eKͨtI08ЇyˉR}wIqFNSXU};-U 89=aH$Sލbol`iuW ;skgE26wwLy_[!:}p5LYa..l1_4l +k]\TEE*u;Bq7ٹyA@/F>ת(bb2R6}tx[8<:P1 tajbfrÀb7X,7/۞RrGXYŠnnD xY`n?{< v@FF.@1`Im$WdټkVYFO8hq: Ofo(Mm-CEL=N3TۈXY2\هMghVN`u} 8PIFe7fZ0Ѕ D2L6DqO-E>"`֪[Z*ƻ%3r:051jw݇qOb~qb_R՗nkz,"[}=a/r _zEQ:l bw8ll! `gsye]4 M<&Sۖ o (΄o}t:0_ϋ7ng4Q,DIlX' 7\:فS;v=M6p5\.'EA6nY<ک"""""""jjPyHh,m?:bzlR^3 ?X<]בa#j?g a̻ӯ0>AҨە(P\V_+r|v6b$mrzEQ-U/htZVX$ɜ6-\.UD+@Q3D4 ܴs< 4 UWsmcǧg٧i( Y7;n=a /@k뮪*R)T>}ձ0 ?C}J"""""""/~K4zC-ˣu^MFk,ͨzg-ODth[ \;NcieD:S|Lr@x,KvG(˲ab'תm;$oZ/Wk"!l6+sFq Pi-KլY9-ΣuXj;\c1t&[d DDDDDDDDDt1ODD≊ɿYv9t1 J![,}ֳ'"""""""|""5݆/] Sow?Q ߾~IADDDDDDDDDD|U"""""""""""""""'""""""""""""""G'""""""""""""""G'""""""""""""""G'""""""""""""""G,_DD&X kҘt3Lgݧ21t"""""""j`F o6x˶bmuT\Rm)K~\ojN,F """"""""""";?%\Q:&QfT=PJGv3zNODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDt0ODDDDDDDDDDDDDDtXtH3ډ BE<źSlK2zC_(Y6籷w}ֲ躁|YKDDDDDDDDDD{@>gH$qpt!Ȳ xa 00A*UMN}NNB$+!x"\.UFgGd9LOM tvΟLwi9T,vDٶFチ,O""""""""""Vx73Kg;8P?dYF*F6Ozj?`U W'L1a "B0Ѕ\.>¡.'FESCoy}_XF(u e̺!HA|"Jg `l3Q'".pX,xx<$IH+( DQ$I7.KAQ`,7  KD߼~^PKblFUUj^"/0 ᄒT" <{< ͆_}g-N$Sz .l.AbzrǧH3xd" (_odv W~\^%tiDķ_ L YaP(EϿA7t qc}sD  _xM.0=9` {GލB|s{6|Iȭ:"{@O>z!Hb|tPt A4 (֩t?,a=8]bcs -H "Άy F3Yy>gqJ@sڍPPW!Iҙlki޾_Yج2/.Q ^ 3(//VYƓGb)kn~ \#CrpCA?{6 cum }xdziSVOp@Tmbai W|^<(8>= 2,zcTcY/"IH$SX^NS(w Q1L6[|)G&E.W/@e(Qv׃'ds؉^|A@Ӷ٪ 76[[mPo&HlEl6=nezk8:>F0>jUɔA;~-TE5[;8<>:JǛ_Xg>߉Q ߼anyVڍp`@w2Ӷtf$m˻ '3H@ӵnh dي5=76#f%}nWqذ$IsX[wyD3'\~?f'OH-Gެ@D/Κ#eY*cC٪3Lú' I^7 kYUm>?6vdi-tu-KE.I*:qLNuCQU8v cai t3DDDDDDDD0OD'J( ޼_0Ӳ|[Ze9oHz:ߑA,nǟb=uM3z9oi^~5MR`Y n~,, =Mu.(n6;cu} dSppr8r^uʑLV,%0 Vݬ{qx|}=a ᰗݼ ;oD +to7Ht= T-7CDDDDDDDDDD`!x]uVl}SNǧ( 2puFk\z"=6vx)lrBA]8@WŨƞ>y#u Ynz 4c؉"z!Rs ۅ>DizېpyGOO~02UF:EWgG[rv~ lA>_y(S]]ڎ@UTtt} +v:EP@0P傪i88*~y;׃d*T*tIۖcxG'_\ 2,vpyV]PuO$!; :jkx_0{Aq/?&ַvc*faieX-d P< I7_ Y,ܢYG3 062wEn~lr:051ZwـF];n~LM`ai?s} sNdk#ԟ?<7a|]d2-;.ܦ `iutv0;=io6[w*nXw;=DK;Qz`|m~5M @>cxdmv^<}EUa$|l.U"Bú*v8)䯏jdR7wW7s_kFu9, &m~}7;UP`o#@6lB@DDDDDDDDD{?%\Q:&QfT=PJGv3z "eK terU0*mUnYo4J k4;bл(p9*"b6zkZNĆkW_GA~ScV^m'O(863Y`Xd(G;SI7oYY$ [fm7%"Jg0y!Lqv<n@&a|Ytl `f@8,kYT..14I7sp9x'}tO ?, Y;+븼q*W NΎurv. (v p] =:>E:P{ǧg8]"nbS\% $~~3 vUfs]\BEoUӰp\.p(>%>D*BA?, NѡO; FRid9~>"^|Zȿk!_(T v@nt(jȟA+cnaK]@6.vJ ˉ:h9i'Hq@ax7A%=<vv"cf.vXZ݀㮻!L=( 88SOF%~ P *J_>EJ$6(`c{CLNÇt&ϡ ϟ̘ۢ(UL6\.^<)#Lr(y.LNB`mc>>/ ܇eׯAQm`~q߾~4}zCtӓq~.;0ntv }88:AAQ!2R4 u d2]7Jz`|#mBunt[;v;SlD!@@-c8ӀkKۻQlF(b~alcݵ\>ܾr pW"{p9}'~tp(8:9kݎ!X$ xdBLftt3..>#˛e2( oYV`tp90? /"`/"Lwש0heu]OڍӹSul\N5i8Wյi}e̥ۉ1Ͼirgt3IWKKZ^YΕUe)0d ґѡ~ (ݠc+vQ՘ejMM{ǘ/~%(o[[ RIk@¡PٲXY؊e(h, 67傆eMM2 ClVÃhTgN(=yZ谞=Uiɺ]Owz;S=~LǏ:X666+ٖXL KfsV^I*<+ޏhSť|!諤RE&S x$@o3<|>uw:֐fF (;@TknV{[f=@ $g) mln0ijeuM#CuVefi{)p(e UTfU'*Jڍ%I&ON-܊EϞq[h<lnƕM)W{1XsTGiӹzz'JñG Ύv55EH&Uhzu T_ J/T|t&w\c?2~>(=o};TWgF4%I=~L-fCz4TwOkdxPplbc}hSD@@_,9T*p8::U_oMU*RoOff)NtAQ `PHgN C!KUh `*%iq]vSNLEf=꺦NPch͸Vkn+={ёGF43;hhO KZ^Y:-fsZ[PWg{~:9nU[mۥҭڢcý4b4Q[[~ubzPH-1m'pxDJ9S'o( * ʥsA>a@@$*%C{g 'Wdjs4vdD6J۶j|lD K)E<7n$5Gu1E"a_֪GvYi7Z[bt~JwOk7%I:{TEc@9=Pj/VW;?W4u=$ij~0 }pܛݟT;;T.Ie&}ɇde4l/ JYnhkmѯ>ӭ֖.?SƒL6nU.+=*xi#fsz:BϞ0 i:]?I~ *’vvw١D"QZY[S*&iqM:^u=0G\-?22>jzl >ypy7-֗0oڦ^˻}:hZ:Slxnl{-*gUrgGa(WϿLFS76õKH|>ݔٽV^&djjjxyrgG`ytCrG2p(\Vi|}U8rJ* )zeYݾ@[r^񆚣M<]5E®U.WrgGQLqNLoԉc$3Lnŗ_~OHJ;2u2t㕷M-myK{ߕSoI0X[mr&aH: h&ҊRkdhPCB _C{$a]}/g=_C_ pi*=p?!c~g}U]ϙ7M{u=?HKRiE"N$`0:({bwgWO=i( +M&}Z\^ꚶXN7UԛaZTJ`P::6(x D,֬O?mw`{˪ם:zTA݂@mxH+ B@!@o>Y]]$uwwWN;N;oo>N1 aXg]M˼e:̛iWnZ惖v?:9n@|$鎤޻+cY'ci9L<^ytҖ.˼]*A=C@h 4|A> B@!@h 4|A> B@!@h 4|A> B@  _/T@h 4|A> B@!@h 4|A> B@!@h 4|A> B@!0MC#|]iok1x=ys̀#Cl{twuo~hldH1 M@ҰQ}ѥ5냋gU>ŚS_&ucU4]4N ]YfmW{ɐID 똦H$,è?>e K._RO`ԩmιYw>9G=9h~^$8k ݁w؈&O((P4$4h=|\6k;=ݝ|aJѲήat{ۣH$dronnsgK=n8S-y;5>rA='OG5eFunPm;niccqvt>z!v"?7]8f]k[^\붝oŕCÛyR-y\EU'PP_|Z[c_f0 @#ȯ?8.t%ChSW^*aa]<F;;=P6UG{z%(?n)Zz5TuݖXL=--( S8zD+_x&ۣ[wkqiUGFtؘɤ͖mGN9ڤt&SёAS%II]0?;_ǹеK=]:22dzzde_oM PKYGG4dbΎ6 HR jSmW^mlӊv7cx=s^~KG-׾FQuՉaËjii>htxP=W(PaT?;55yBn?pNSSDgϜvB_RҲxBAǾڥRi=/ʚW? kV6U<]āijkma܊+z0v.G5Eň+4M ߐZ[b *}R(g3Yr9ɐvvSzxajkiQ8R:V|M4Ra ? èCr\LEM BT c{4 mŝ"jn*`o'\oq{8~5~BA*ks3^ťߦi()V68mxM, JӥkTN:;eHڌo+] p}Zu}) 2Lx lPuj:q{eq45yBgL胋g52<%3’nݾ?dzJ&wsؘ߼S; xFKm[qxRv8~YEȺnU"T ` -,o^ɐk뻫7 ?V,J2?)Q= S,{q:W^xvrb\~NM]~SDyyvN$ŷ?b\_Lrٺ +<56* νʚ>rA)I|^a?wt<+[gh_M~lV|:w'Ҋ IDATFd?whӿWeo={7HΜ>e#nȠ.T $e2Y9e,7tuuO?,Ҋ~~l==Ze?.=##Cl6?+UĸNP `ֿsoZӏg=֢|n޾'KOm.@?|^_Tvi뻕ߏtrbbh]tN7QuI_߾Vo +(V\XRwWFG+*WVוd֦drq[{˭\:ݽH Kjt)}?UtT)b~4\xwW&ON>odZZ^UoO (m Z\.5@@X^/'%twY-,.+i|lDGFJAR=Q6UwwNsg֛޹f+ʭƏH^ZBhI*RhS)pr@_W\zv㶶tΝ9 oH:;絰{) ]:FeAU1_XZQgGOifv8yvwSYkѦO>W񧟕Ng4>6'Iʾb(jW|^OiRiĚu]0[Y=pHakggWH{jՆ?n*Ί孭ItdtX''5qlLΔ1=9-,.)bhYhnEJ$wm98:ݽ?2̀ZcۨicNN,V:Ju]0ХghVV A*i;@Rc!BR cW:_e(ꮮ jż>z"I܌0 '{ӥmh:7uJ'&uc_@zz:\Q{;%r+WUۭޞ.ͽ׽JnƦV^I^mlMà Ce:o5C8zDO>/IF Cۃ|Eyݼ}RH$~Rh:uvwSzٜ66o>Xc#ҿb0N`zFC hZZ58Ч;N˂'$IW*=ƭЉzLwy”Bߓ$%;zhFOl`}YۋE]v濻Z,-ԉcd2w CKzjS=jtFK{POWcv,QNj "IZZ^+ ۲jn.O(]浬]zDrGzp gr]=s=m e%Ð!CmPޡT4Myˆv.2d_Z^U>WWG_.,ԩv4<_ {NT GgGv7FmT>C}r霖W+Sq L}%۫?9{e;T6UuXުDlq!c붊CRi o1=ccJg2z T:]6~gGhqiEǏU+#C:2:azS4KCfs+gڊAr۴R aDMVd'V AX&O+]`~k[?2/>PrUn’w}scK~ʶ5GRwW"^/:j,֬czr)}E[2 C--Z{P0>xYtĴQ'Q6ĽhSFtFDR{I&wN(-LF˫Эdz{s)*d7!kף+kem]x|&wv%I`HICBwvvU+4G١~Y)MJg2GBAAĚ+N҅O\BBv>rACҰtFὀπ*.^gkkL&R>>ZbN$duij;̓9eyjc48lVۉZb5WZ#zYk{Qq?)[⊾&OוK;uJQssTa֖z;5<ԯ/\K$ aWWgHHXgC!ml̓g`rL}RSkK5b F[Zbymll~rL=+lI*=KMCWCg9ytFlVjL(R./%m'|L'JZ+2L)*iʥsJgҺvvMrbbL-fK_oO>n-wLt3ľؾt&BrO( --껫7Jj*|TkK+(=F¡}]SjVj:ʄ-,[S'uܤZXpV뵯z}qͭFG4칆5b۲u5 +冣@$Q4 %mw֝!oܺ}M+؊νi!:3Mng٬&OMe(jz{ *M{{ʞW:6~DC}e]r&OM(dһX[Tp΢>%)r9}w.4/,.+jhfNh:;56:;6oU5}. *T3zr6*d =W^9ڤX6dYN?7sO)CKag[s4l.WQE~P%IQzD=pDQqԁMנUךzYۻ>m%;tTҪTx}^$뻟ߏ9uv C!>+[no2j7_׿^?}(I?4*hqiEO>z95!tz8=tx2;u~_iɜ^ml0XszO?kզ^\ЀgNjqiEXN̖mwltXtF[[j4yjBLFf7xV^>Vxc;ԝ{:{ᷟkk+hiA~rgWۉFt&M?X7 [˫k$=W>W&Փuؘ&OhqyEjkmэ[wkhTZGFʖmnU!T|qJÃ?~ku#}M?U<`(Vݟq Cq?{]*ȳg߾P}E{HtFc#57d2Ţ:y|\BFKߊGH&wNhx_K+Z[{ضjC}pnRs絳ndpRӹ:11_}xIO^hieUPxCt.6 e7lŷdtȈܒa BjW_\.Ug/.+s/꺭L&/4:|W~~~?n枿 <~T+.-+jokџ]վnËzܛ|>!ȯ<Ξ9sgN)*hgwWh霯!gis3gOkDٲvBѦ&Ri]qG1IM_yb9[+_/s=?ِ?yյu +ҊttlDZ՜~uG^:+IR鴞ͽᠭ0sowz*6r~t~n{`0tbb\\xZ{nTŲG3OKA~0P>R!uܤ.,s"B`r~bL6P0(0絵 d2uff ϟij5Kg˶h#(?_r<oI]0UUO9v-MM2\NK+J=w֭0M D4AoOw=y^?\ߪ}W~~~?nٜͽ#pX`P ~PPi*-sx]W0 z w2o}mۧ*3uOlA0(<>9~5`@m-1Ui[i%ּW Yf55EBgtzFvwSi_m}}ΎvGs5<٢8trgg={ j5ks+>}`0?}I PsItp# PP\`0XT:P*Rj~yA*V*>y5GdRMMC!eH=ṙʐbQD"q*2MCQe9RGuE"J3y}e^߭}4MBAcv]Fzn^$ݑ{wze,d,9+o[e+_%"2l|._6Աzx`@pHHDǏ9;:m%wv0VWg^\w/nhL&Z^OpHm{ѡI6[T]N\._1S[6s׃߿; js3ήG7T1]VYd>]ߝ\ծq-]C- ټJ굛ɓ \Э_` ?TJJzT\X_\6xbZb1E"ae2%ɚcI&wtƝ ?wFZ[\'im!\Ƿ\~̷A> B@!@h 4|A> B@!@h 4|A> B@!vބ׾y]8tT@h 4|A> B@!@h 4|A> B@!@h 4|A> B@!@h C;|/!oy9ٗ ^psFNk?CSK[ϞfvUKxV !o%us7>jao*wz37@Aat'c~䪯HN~rZ3n"WN`_g9?:hoOpoݟ>W˧8 "@]Nm^'GaC,wv1'r k suۯ  8U-w[٦9߾ױGMSC뻩vN=ŀiY oz7sv;7A~ ޭݺ<ﰾӼl[VV 5Y-A~e0|+u W ڜ~k/e yPRe`oSu.ڪ9ewg-[W{Mshj!@if e[f2/~^/UoC=s̢݂eZ|?lۗ+w|-a#w ֑zœcݭs:~i:~m{pס`]{H_-W өnxv|>/Ia ׼t^.UV;}N}V"~>[ױVĩ ߫^hTvˎW3滛WsCWc·W?T}ov ʊ+|{"۩ҽZ~:m߯ۼjh{+mN˽>k_f)4-ȷŀZ_-зOZպ[To Mr1r>멞n=v 'I< a}NR6o N0ZYo-ȷ6xx T;żnSU;Mu?sSo"Mv=pp^IWn߫Z߭)%Mpi5ܯv׃5ķmlˊxŶ20k%n^9= v w~o[x<,wvsnnj{뼵i8k_ Ua UO]jȷ;U[{X洮״Sߝtn8[=*Bz>mUgֳOۃ~0:oWE7è*w [5VVV/8ž|0xߩ_*̀* +ӡ[op ۨVoi%nUyJx|)w ܷoۭ_cqU`_ף"lr^k=D큼R?kl1/ɩީ?^ReZW ʩ2;W:rYpWzNz9IkoYȷ[0TxW9ժ*nUՆw0]kۡ:H_ ߾^o UY/V[}~* O^SuWӐ^ժݎ}w *˜*VibXPeooMz]o C;U['j^AWsY[eڭ!lnuZ{)y0^n᾽i}xRŠߺmS7N/{^A~=qSz8o w-Зm-跆֜٭߭n˝T=|s~Њ|0֠|6VomNnC6O>[[ջSռڜ;mW c-}v^=wT4~:]:߭&yixyؽZ^N۴u?i_ASս^uVo7-۰Ne^mޝpO]m{{P_*/λU; VavNM=Z{|+i0>=\/6LN<2:Pڰ~㝖٫ns ^dwP ? è5N[_ 쫅e=vk? o}Ox8~ܫ[۝䷷;U黭->{|>_g=;M[į5̷_ ۴uioϩZhO}5~qojCʴ>܎k.; 5ܾuzaS~qeOho}5~ pua m۟u!</|rx}jϛwj7}ױ*]2Ӽ[ ^n9eByav+ϰR}w[՞Se^l7ֱVY߳*R_֪|޾۴S ;U;WP(*%Ǫ|{y^i̾]{UR*|z~3ܾS[eJ}Oj|Nv?vN@޺2܂ô|ju!.weJ|:;ȷpնQm^rNlmy{ k^n yk~}e~Phh}潆 _m(}}y~jՖ/W'~WjW?аR|P|20i >^vxW;7`/ {o=ėnCewZwYn}޽}x}6A}wXQ}|jϤ l2CsA=?̗-wjZﴎMng Weh}ux}!~ڷUF! ﵌*|?C{-S3گBckZ|նg^}r[xZuJjk[B|>j ~ݶ綞ۺn;:K'hNi] ~?ϻN‚F 򥺇ja{i~V{nJ߭{݃|3'w :w;' P9'r2&U3KB/K_R|I;:Gt MbXNO35;/d{\?Z*}۰Ǧ_d'HOU[Ϲ񮺝ȏ8=.O2?gWiRcsi$ s~ޯɎkȏr;Kh|z:jU?b@__t~x? sOOw kXȏH\mO!ߦ6UQ~k q]Wmz~6oDѦ2~5o="oUDj}4 O[ { S O {G}nh#CY>1֟_?_K!&"D|`"B>L/rPIENDB`circus-0.12.1/docs/source/for-ops/web-index.png000066400000000000000000005710441256046442300213110ustar00rootroot00000000000000PNG  IHDRb iCCPICC ProfileHPTY{ MHɱ4Mi$fQDD@pȂAE 2(&Ƞ*ک=_:sλ@>` |g0  skhvc8q$ٜ‘V"§US@9"~T,# "1\0ˑ|d.&aDOf2?-萄sٱ\HDɳ9$&&r6:o"M&3Z56?-1A edSgkFzb,p TgMebA ,rZ$oQ<7rH?%lbCqu[`~(>%-m1e1ɟe9 9&wpRM 1rD Q|E?&_/$d3]E  S`Lq*'#u6y$^&?6:&eLML}]H+ )ЌAܢOb}82P j@=8Nv.@Fk0 >ipB C  "h lh;AePB>hz C;+ɰ4k`:{ÁZ8Nx7\ Wp|߃kx P$,Je\P0TڄCQ-NT/J@}AcT4 mE{,t2z]G/ѓ F001јtL.S9|bXmn`a[Av qv8_5.nFq$ s%FymK4AI!؄LB!(p0J&Jv@bq+BB|B|O"H֤UXR)8i,E#nrBhQ)aTnJUH!,V.&v[8A\SI|xx) -  &rCSTII_DF>1)[*GFEUPYԣ+Qi4C:N:_LLL9,JVK! [({R%JKpZҲOrK8ryrrʷ?U@+)RHW8pEabRۥyKO.}+)+nPQWRRVP)T4,\|^y\bRrAMDK.&UU=UUjjAjZ՞Q=*+454i414{5?iikhjӖfhgi7i?ѡ8$Tuu҃,bn 0\j!Caaᰑ6v74-ۻwc ƏMLLt33e5m60{ko1?ljbEwK+Ke帕UU]G/_X;[obcijs/[CxF۱9ˏ.ScU iGLjlZǗNNqNNog?ظltvEzI=sWsvor9Pb I/+^e}|>+^+xRs%we/e}**U^gP4| t, |$ nR"\l7BBcC;paaaSk_3n~ڌ}%;^|=sLDHDc7/9Ɉd^q2.(j,.z_xCLIDKlY8ϸʸOu3 ! Ĉij\)n| !B!B!B!daОB!B!B!BxOԻzp cyB!B!B!Bٹ%C`= B!B!B!xvt0zP A6?P B!B!B!BP:~g t`pC!B!B!B!t+(=tg LyB!B!B!ByLcv 8B!B!B!B!Q.e/7_j? !B!B!B!dx).7=Ԡ}yy C m4B!B!B!҃u@= 1?qvuЩ׀ [ rB!B!B!Sny o @{'B!B!B!|Pl7 H@ 'vD0 ~oB!B!B!B('fZN[fތz83:=+w\~ߡ7K?7ٯ7~B!B!B!B)PN0z o}onj+w@}L?Iq3|[G!B!B!B!dSڗ r< 0|E7 s}X/7_r+_}7}!B!B!B!dWbo7Kmy >+l{̺r/`Hm'B!B!B!@߶/ @^SK_r ;~o{B!B!B!Ag@W>f[Arq T5\a s o[go7]l!B!B!B!N@^NO 5 4]n?.8_*_z3ݸJ{0!B!B!B!dW&.`ml3o#v\ f\욊Vvd ߤ7 /7_X^▋O!B!B!B!L_NT`?nޛ7;,po2L?)K-I5?"間M'B!B!B!|P)lo7; @<{K'.rgC^H/}9oo/}c瞋&(B!B!B!B!; L?dɒgo./'=nC ׁLmH_+N:׿gLq# 6k m}?֗oY|PWr3mYz^y'3}צ`^ն5~IKL?_!ʹFR??????c/u;ի9oXjZ~q|Z0vt n-˖lضq?я:XtE RH%h*sJz٬fÒ_,P2ЍHŝ#w\q0mQA8NݹHKOy_cܞ>Kv R1qf#1ӊk`nG*s fC2;`YVK)a1]fv.:JGquPCFOOOOOOOOOOOOO#< O#ۿ͘UbbRqfYLFkRoƊ62֛q} ` 6< B+]\/xQxYgvfuՍ>"KcJUEͼbe']r\k+K]NOߛO/_uuY3ӿ^OeyĉGXB@7"Qzs`e2@W#4l6[1 }ܲ>^Y~u IJ,/=y_ 0qqGaqu1 7ƵOgadHZ:?: Ͼ^_^|Lso{qU~@~zَYo,[r>[/R!,}V? hc![6cf}Ї茳~s#llڕ/oߎ ='^ Ao;[ǜpis#O*l{WУ`Ś1~QcXlw1s<|30mّFb62i\t:f1i6,)"H4'fYxkvfGIW\2+b217n])q6Ӑ27n,3?Xz5ZZZ0f??${1,\;k⥗^ƍQ[[=ӦM{´g}6:}]o&,BEEƌ3gp]v̙38OOO|eFO֝ɓFo gBEI.Ab5e+f2֗ ܛK~oݴu6?LYtFr(h~oX_p3,4QƇ~Xm#k2|`!t,>uw?8V^ŋgg'}]L4im|)W^wߍC=ׯǪU_|L&J???FOO{K-cɜ&q2-&yXeG+,Pa sg`=[V,8$|ttgGxXt_Qt\l>n&ܾ=$;Xc 2+yx'f80u}Д{J0} }z,Kxq2wiaz@|Ǥu- )w7|3yyttt/_{W]uo0?T 뢫k}Yu]?HXDҗD"|7x#QWWݷKO??}?]ο ۿ}-V~ )hoY$E7"z{\2f@Vb R7Wrysu+7ۃ|G;mUb+/"8HWB5fLCmu~ɚ:8J@A 1{ApFK{;a#QSW5@ue=$ ϢusH\اoik8Mwx8yYC;1i9zɓ~JpE/Ǖ]Ŗ-[¼-[ƿbʔ)?[/Ƽy~}]- 777㷿-z466ba0oڴ -B.+ǒ%KpbΜ9q5ꫯFCC,VZç?ilڴ ?pcʔ)8묳p=k8`ضoƍíފ/o~eEʓnOOOOOG0 o`|*bk;/XEЏ/ۗ#uշWt**kBEulv!:ީ" IH'PِFE}-upR)@HT Tl В8LՕUJW"T H!I6ʮK䚥eoyH7j4 Ow0(uo9G( [ XsI)_k$QI݁KS43?īٱ&????濫 m)qxbL6 |7oos=ӧOG{{;4ā8<@"@xGpQG.TWWeҥ?>fϞzXd yp , t_W8&Lo7nɓ8N$mGuu5."~폿H&8Sf1sL̟??88umMܵf7^z)lFEE=PydYXnϟ 8=\̝;cڴixqc˖-xqI'a͚5壏Ljjjկ~7tyuY7o^cb֭%777#D&yL2---a~l6<:MS[[^ohkkÔ)S<˗/bqgy&Ǝqơ3{?<eL4),S۶1bd2v?$---mC_3f@SSCllOOOOOL[{|?D/?l-mnl30طz3omT.aCvضQG#Ѐp˂׺9'֦Ns@ O8QCK;+pR* bnƶ3 LS#$\ڣnҐ+_CI#tG0W׫uLT+y7????濾 xꩧ0eʔ#ykllmw)3Y󺮋nvo|ڴid?W_>Or lj؈ 6D%f{}7o~ߡ#F@.8} h8P߽rzlƛ}c9.f[-7_C 䗺r1q} e3oT<ݸGNš'3+Wao*b@u-fq Fѣ;]jFa9'col1MnwE{w"*GeY9ՕⅧV`ω3f wwFnTdjDiҠ?WdnRuп!Kg;/IC+IaC!ljtLiJe>Qd^[1r]ڇ(ο^'ڵPJS.R^z?8CJpw⡇ˆ#f1qDvi,+Ys,xOS\{B*WU$ tI¿)zmjj"L²,|T*+?qc¿G#<A裏FKK  㳟lЧrDmh"\׾㨣ߏ 3g΄NHԧ>_Wꪫ>s{G9rq:06qA-SZɝur|Gaיy쯟pbŊ;uCJu2݅_Wu&@;u&̚duMI:&R1$vl^ ζu0j~|8ɚHCkT_x3qdrXU&ZZ0rxL3{OT-,kk0\i]_^/o˲Ʃe䷜WD}\'-isMY\۟eY_W0Lcuŕ=?????N{|>.xѣGc۶m 1"r.qPWWl6 ˲Nm6oYKlLdhkkC]]2 :;;whjj˜1c8,aYjkkL&ֆ#G#{thnn@cc#ZZZ|CC,|G>G*Buu5ZZZ"4jkk܌zr9tvv5jn⿪ hii_ʸUUUa^ ˡ۶m ">#(rܠ'r[VtYvn\rݾ__p@q OH[dgٳ?  {X.ԇ [s9B.VZn,osAxe }qs+Vȕc^T0y^ ReYAaDIyt,ˊt.TXqmH"ɹd9QH:r`66A: eݡiXqr_(Nk0m:Sϙ3lDłv }~}eD78o-c]T.ߏDWl"@o78H'!Ia6#[*j#O}|ӝ>Y/n0mΏQ~z t$ϾK_;Gݠ-:Z]V}qeq;Pw7 }AWz01:>cZ~˲e" FY;^yłEoޛ/;%~˧3̩RuՍ蝚OGX1<+y^l6+v9^7|>ɗp$ }n"uCrn]V r2η\ttͲOqGwSnwIQ3xliƥ_֕ b|3ou_*X7ݾ^7W^Ru~a%z+<9Sf G*~DҔ 8Nxy H*eYaoo}}҈uC7_:I9H֝#N73Ӌ`g ?2zay???|??48?w;~۾XgNEGC̲, K1S뗋}ŽyS3F" @l} +4 y^PsFZ1f6vyE҉WΩ\KS\Go]fO1J7#H~̺_5"#1f+s?}]t^ <ڥ.CӻͶ&]w:F:Ƅ L&rڐd~N ^\Dt555HHR}Jڧ趫Ml|>L7,o=} XamʹeuZ;m]Jnr̛ -9Yd q"o2PflYʇZߒ^Pd/7q-c> LׯG}}=NRObR[u`|໹ΎYs}[s]lT ذapG#χa6$`۶m?~<1k,yTTTJCz' d@qXtƍ  2sE?l$)0o (נ]:<GȘ>@a =͏lw*NL,M;2e뺨2\I[6>x^GosR뗚Z fYA\^ޚ/]cu@ߊgSO=iu]XÊGA;̙3[oaɨ5=G=zPу&5k>q#> ҵɟj_5)L+#~N_uM,'Hr]̖`ްo)~7/:}) ¿x/X"Dׁz %K Nz ;j.){6]9dBѥ#kںtfzps9Xn̙t: qJAyo<ˡLUUUzD"|>2 9P&OɌ2ʠ*Ǻ/|Lbr!/yYt<@,enO#na E6 V4zK~bۊŗA뇅 8_Aɲao;˅}r9iHx~m ~18/x7q50aMޑfns\8g8dATE?gYVdx\|T* +<#H%xXV r]qQ2 C|ˍ<7H$"y>oDd cMotx]|>}lMO.8ڿёzMr ƷgEv3@o.CKk %//Xs['"|lA5G[1|'\<777mڄzTVVL!a@_q=P \@d2@zIC%M9_,ˊ ^W$HISΥVnD<|<=@'I69[!$A$č$>Ͻ2,Y;F:zt׃+D7dAG}!!Le dsU'1<ߪ'yZO?I(<'}?|Nߘ72xJYJ۔@8K6=8voNKnJo_F{-׿Sn???????????Ƃe.[b׽ȷt/sR[.}"|i$/Xҍx}c|^> 7( o̢G:9{ܻ /_D:FOO*++/7 Z,_yޚ<@!:[os̐~?O}x' 1cpR~ ޿O#,K@d@g}" rz0Au]TTT 4z0׃ 2 $ۀ޿xo=x.e O.d8te`} $rJd_]2`q捏%JHw#} 7: SߘuȍɈ???????????PfތKXj}f@EW@ F~9~// b~'AlXycs)s{qxݨ?Ɣ)S([OGmm-.]yaƍxgJpEGMMMz/ԧ>^x?q' /c! p7xɇlꪫsᓟ$RԠ<|_.B?I#̛ߔ)fd:epN# X*x^aJ=qW4t p͛Y',R#7 Z<āTkke6l8|?Ç_[ɟ.[}c"OAyŎby9\فbȹqAbqOVX,i€̏}<?aOš\zn7~6oկ~6wy1cЀO<T /q1`۶m< ,-|Sg?wL:O=Z[[`p }> ,X^z ')S ,[ O?4 |3/Kr! /} .2wo~X38l7xcz$׮]{bҤIhnnƵ^iӦN}vaap ;xb^?;g}6?p>2{V \;͌e޸kNaۅ'N Á-K. ۅNOr>xe + b`)4@2Pʠ(}Λ8LT*L[ՃΗNKkzopAY^yRPHto9^ާ%ok7zkM&o)/S+O&)q IDATxccu9f0"S ŭ+~@ W {1bO>;ԓ KS)>R)WۆSbD 8~g۵p3'lv5H7NWxn |ghhii `Ip}_|quGOlŋQYY~^{-K/??bݍ*XJ1"g7n\ş'̝;r`޼y s qOS̘1_N; A`xqףϛr .<ø뮻~zXضmn6_ֿFq$uWFmu{:"'O>@deer2uuu!LFs=t7$ ޛ \.r!V_ΡF\s\XV2uʶmrZ] 4=3ț79]jG\SI:ӿM<(>eKHfOOOOOOOOOOO?1~\0?0~m㊥7h3?{R")R4#*ml.gLണ:1rU!p)v^9/v%O?Xd .袢AߩT lf.N0r. ^ɍ죯Eґɗ!} l6̣S۶ݧ |ėy-7r}܄)8I_"G#7{COOOOOOOOOOO=r1gs-fK qgky\'*"Jp]===a }·n U^|>l׿5|Ir9[sF# {…[n6mB[[Z[[NQUU_|@#hooqEn@6իC ~;҂z˗/G|>xmmmxG0rH̜939/iXjUXAK/+W / _d[>GWWlXbѢE8餓fo#eYXf >ѣc;=SeEIe}y([nEss3s˖-hkk֭[.$ ){ qO@l6|e?r\C Iok2bC.3 ЋNK- t$ $mGҗt=/:g>.}%%X"l~u!u oGpI8#uH5sNw VX٤`;2Q@ +ƒF*@*TV8ш#}l޼D oh ~r_Vbk>xmLHZ {/~'8?TQQ|>*yTTT A $d2y, twQYY;::0rHd2x^IjtwwR)(@˱&)r9<voz([1|P7@$aG"@7:P}|)/u],[ \r l_l~@ܿ}v ~ۘ4iFq†;;>Fwq 0׵ؙ3>jpxWqI'<Rpd@”:z6zGp|ҥKq.e98MST}R\ `ߣ OiH0_*G)}H+ g3aۅShٕLٙA$o=8pO“}mG|>+9t29p@rHR72y/1²0ϲ]k<8e0u'<\< Fnvt^ur#lFEEE8K>d uT:fAnӗAysk7@A%-s*#˲kH/O rGKqbs8RޡDl _t0ǔs\93nJhY'@mihwcm`֭ ر##ƍ@Mee%lLYcY@SrU-;% d2Td, H&HR}?hd`TeEFi|ߏCo2dO(@, 2 0fYAHYI_,NC<ȃrr~x`v OAI#7T* xvɣ9'Hn͚|???????????GX/@ *Z?;sj}Xo,ZAbM?oB*mww7^}Uva?>G O?4L2̓ȗPU?(L '@8Ȁp _b<ԩSQUUyM_|2zyNF} e)H8%/<$zPk'}ߏ @x9#ʍ#_AJ~\05PmEX}.Q7 2up*GOOOOOOOOOOOCg^ `JOoNoNoNoN/3>J|#B(]j{Hڿߧ*,Xesh֌+Vq|C?r!F&Afdz<B`[,Xp.ܼ}ܼ ۲ܼ {>?@>=ͻ`a)Xz Z[ZlFIv>GGw<؅"@/pz rc;-0?R>r| ۞'۲{> m '6HFYQ!tdk0>H8 )$DX]ڑ H9N|>PbSqZ%X5 .NAaY{wcaٲeغu+tێ"T%щ*n݊餼38ƈ#}vx?~<'xD9O_@?;//`G?zZ숿GOigWPvΝlݺnV4L/@QQwMMM[_b񔕕%b#/////? uɴVu=bz|g\y\dee hfVn deequqWg~CAiK5^]->o Eſ_oy~//'o|~-?[?|@˱ ɜ'|;nR3 2|pN^z%VXS    Ej2rHƏb07t*eSX@mm-+!==ÇS\\l/߿:::zCꁵ ±BQn| l)~?eα'YbŝLw,cS$=`=dC{%bqp8j5jt%;z5l~X?AC5lܸn馈a(?O^^]tQDCI $)2h1؆?[zԴ6Oph>aG'0 \+]vQ[[ VhW__WWRrz᧚~I&DGsu]'--3grJF3x4]%8б*c˽{2c &M}7NvUUU܊Xz<֭[GFFseذaX,TUEQx:o"Nff&TTTG}'LZZZʘ6ʜɓ':~?)dIOOY'{ef<]&Kt܉F}e;3#vct:}zu.9Z$K*e`ۗBl(ѱCGYGJLGHE!Ç'޽{~Ư >&c_5z~/K, NUUijj_W_K.᭷b„ q#C@ 8q:X,hzM^^MMM8NE>pJܽnʆzJY-`s~77)>vk[ZqDcuЕi Aq#o2_4]GGB(~:3gdժUŋsEQTTtl@ %T\C"?Qao>JKK_[[k5RQQAVVyAOTilٲ'v K%hQiپGbWnX`&LS-?Cv_>t#x'?x,Y1/_lڵe˖1l07o#G4Oٴi.JJJbn{S\\l߱oˣ////////kFSSՌ=|nqq1;v@4g~24~iQO3A]js5]k.Ut]Xt.˗?"fϞ9CYY;w䣏>b͚5_N;={}}a>!//GO]x'~Ǐ%c3FEzXw.1FFFqvKDMM 4+zOHOONQQ9o_ׯ'//˽^/+WdӦMff&M矟ul2.2gwEaŊTUUr8pcܸqݻ EQkkkO/^i\s5\ve+… Yh>`Dzm6lXoz#QeiMM =%%% Ǝy5jT4-sp8"W|_'-- U ̷} >nyb(罚v.^ZͶgpݜW>w7mbraa"& c9<Á:NyO^tBCk>(A[[_ſo3.]ӟď~c )yr3$*Ȕ)S4 /;p 'pYgV8J7EaĈX\mEcpJiNW K G"5`رy+×%[&Z?Q~o={PQQAKKY1˖-[4n yfLgɒ%\9;wd*^ ƍYz5Gض;wtR^@zMr_S7ٴ_IO?Q/"l}7ٴ_IOƍ7jPXXHvvNvv6#GdϞ=TVV(녅=:LO;r'6\~<w`Jn}Iw%oiiaOĢ|p 38̤C2qD xX`ƍNlVܸ\.ע + 'jEUՈז|| )SͿ]O7ۅ/F|^7C.}\xf'{+e]7Tь;N/__WNGm{x뭷hll;h0٧ҥK9Sq8\e>4z744駟r=`;8<ݓN:^zgC:}7ٴ񯭭`֬Y=s9,\={LOQsԔ"o͛7ɓ'baԨQ?˩TvƋ'SQB»蠸t-..f޽nߊ]024zA#d;ۭ,{fI>1ih kw`StHIi(#*4]gpurssyׯ?fڴiITG2$*t]WHVg%;;s9'c2oTkaZOķpRIJTΪY_WWرc |>~/:W]uO>l2~c x7pw}Y4~n渾tM\Bx衇̼j~򓟐?`…tvvOsWp5:u*gw۶m] o>IOOqm6?]eeeA)++bرc'NbEVuƌC0d׮] t-oKd@r񇪪*p`ٰlX,+v; 6 Gww7(L~_ž1 ]əp3o } æꟓN]35u.F4+W==3= 5ABW^aĈya.{xg83vE?ѓ={gg'Ç7+spu-O7uYж1>쳬X:{.rKļX甪sꩧo3sL:;;y[uF.]brfk8 Ǯ.^~e̙15kx8s+VBzz:7xjL#;vfZ<|ͤaUUUXv Kqwuul>4|>:& 4~CL6>Ggxo͡^nm÷SPe~~~> yxg9̌9Sy'[F$[ċ'z:N .#FPXXHgg'{/燐UViիWG\W>}:_WROU6sŨt~ {:'b0gx@@EEW~^/8 U}4c?8x 7x`n/YRq HE>!Vk{駟6{BQ( VgƏ/gt,®yZ(fN2KA`x<'q@~ƛoɒ%Kl~<<|g\yw}e{{;_~9y7xzϷ-=\ϟOyy9O=o&K.fqmccn:.\|OΛoIFF]&yyyfKt &PUU3gu@Ey;ĕ6stR6oތj/ﱭ?8x`<oS&޲ߟcŗJO5Mޖ/ /)Ǐ\@uu5N3mE =CZ0~ . &v>%:Ttr+yb&S:Ϸn#=]n? jpRy2B_:ZAƢ82aUׇzڵ EQ8sz?'| g߷o_<49]a%0WК%F,СHޭފ|1?䓹[R݌7~u@fsիWn:.ƏOkkkDZdƌcvEff&444 BQB_~eOΌ3Nf^}U:l6555ohh`ɒ%̝;cDzrJ.\w۷/'g}3<_n6X((({?O?=KNNC9 qq1[neʕC\1cMKQBdff;U,] 0m4GD#X8JKKNcc#W\qEO8`(0f%lww7#F`Ĉ]ك+_ N.N'tʘy'[?BcS3.NwS GTT@AWW^/q8PU>Hg'xœIJe˘;wn`D^OIKKB)ңQ*C ǨqO 233IOO:6X=Jw†?Ŵ<ՌK݀E0+ t*] hf#T {nSO=';MMM3}r 333b[fE.?;Ֆ l&7Ҳo#wt4///F|xgƌzVWW3frrrb:GM5]]wgazWU%س, `6JJJbk^*炮cZ#1fA ,~l`Cyjjjwtv|kd 'xllأ";d޼y,Znv;{7C:Orݠ*/@wfP}'N+35FuM#$*-πꎯ)?^/t87v{tJdE(\x18ӈ+覆Xv{z?= \\.|Z\ve1's3*f`{l;rH{1>vʊ9\DC|>3/e`ϔVtVPŽYeO t2 Y`555|)SPWW罹3fDL?䓬[acm0n83n:y}'SOq7rYg2`x)--e֬YL6nf̘.b}׋`ΝTUUկ~ǒ%Kx嗙?>n6MOD74\bJ&DQzG;4i QF ߺ?s]漺M#{Xl]..EffVJxFǓx5{1չ n0 ׈#PnÁ"$t^V+]]]0Fő(X'Wu6b04YwY68;Ոt-BQ?^h555NNN=7PGcMMMlڴH#kƞ={N@yDf3|=#?@ `B=~?ݤ|> 1bv?:.EQp:p ^e˖QXXh6$1cKX9sC9 EE\s //"/b6ỏ'kXքxeNGGcV{<saÆ<5r'-E9\*V6noٳ#wdر#fep0dܹ_K.h,4ס't c_+krmPat-sZylm+@yrݎiGv@P?T(/}K gX._ȷX,̟? @uf͚sXp0Ks&^OȠM̖!ƴ磹 &vM{{ym|E1[hf}QSQ ȷk*J|:tՖqbЇȋq8l߾{۶m`9r$֭ݣ>;âEx:y+7]wŎ;w#x7v2[Ď538۷( Æ \W46lmqq1Vt.R`ذa4556wo<?~"_{KO?////#a4ͬQljN&Φٿ1r=:ގ tSJt{i3rdaѕ >IOš79b5oL$]ixlLaZy뭷q?.X7==FeAr9_o wX ZN#=cF;w$##aÆX?Ƞ.f:yyy̜9ŋ\x<Ï1J(fgy&kX,nr%4hGG)(|bL>|veS"Mݻ{}| [v}:~gChbu.$ٰaUUUX,/}t ~n7ӦMcڴi>C!Qmq2999w,/W^y6Xh2믧?A#}<yv IDATLQ֖R~7HimnkSN)C7661ztPؕPEfѰeY2:ޜ}hƧ~ʶmz<#4m۶qmϛ;%-;;E=4aN\RZs4xU۱_vifӦM|'\pXVrrrꢽfx㍈m'M͛ٿ?KII /~G\QWW믿Ή'Ș1cP47a3I&'|b> fgݬ\E 8qi]'L}g6l ==|~QQGuu5]]]tvvBnn. 樟~rrrz{}_ >~i~i>yFĮ7~SSSf,b%UU޽{ٻwSWބW&Ӱ-VX<~~{nX,_xlq'SVF߱;>调`QEpYJ?cm~7m UunxPz^oLkyxwx!Ċ?YlYD%~ff&]w 7αy@RJKK#Cw;CnhX >m۶Q^^nVǣ7 *++yq[Ѝ}MM 3g쑏fPV_`}?4M'^4.4JK357`|+£>M7رc8Sxꩧ6mwÏ~# ˗/;ફb\uU̞=o5kD#p7SVV#>;3b, ӟP׿u̙]wsNV^mzS4g 45\?rrrhmm3c7n6m=YYYAU@mÝ,doپX___c!//ol'/=|noضֽk9܇^,4li;/e].Ӧd۲hDRykQU3fpg2j(jjjXf Vbxl6nHcc#hvcJqmi(6L2W#tmcoW^W_nSPP^˘1cл'MɬY̞0sLZZZ_vsGue /tR>sx TU%77rOnnSQQ~SO=Ÿq6m(ȑ#Ky7Ybs7RXXhƕe}YrssN;'a^}U222PUa|Is:=={wkrꩧuSƗm~~>?o~Æ hiiX߈:&Oq멽豮C2ɔ=TÕ?m(°V :Ηdb;|MinnVo+۽kh}yNJ;^C 't^n\kFCCeeed˖- [P+b>///ޛd)xЇ%)[ '77ՊjJ%u*s\^uhfffDLwuϧ#FiIff&`裏Fle˖?cs://GF]w^{- D쫟;toˣWo78ozn/)//}w{XN,Kۋ~v;Css3zU999XV, `0!jk=Ϙ`ׇe~2#&/?]Omu5]E!{s%ރ`0h>?ƍYr%Ǭ-))/iO, Yf ]v6 8nx}>mcJ؎7"-묙zs< {7W^y%^Á(tIvo4T9m6z^ȅ^ks=x^zz*r ]]]w}9=|p9s0{ll=}L>nwWtRƌ5\륽=?sFuSſ/&hwY¤ˡD{1cسgnx4n7wyf㥂px<t=;S!co_nCY8*yB:Ӱ,uvtg%1[_ٰQ< tTzNp<+VpF{{;G欳ΊxUL21ū ?DYYk׮e֬YI p饗2k,m- k׮eʔ)fL v;hݟ`Vl6;vvv庮c>|xDhZCj%Ct(z+r?[j@JfET 0lذ|$^|ɦD7TK~/// ?[[[ 8pf󩨨`׮]l߾\<$u/ زNg[pLhG?'PT3:x|7}nƨ"&TTt,(ph!=T+NW =z4g͚5,^[o}g=T>zrssSʷm}IѨJCC]]]̞=GlbV&Q뻟aMk-P30c͋E'":dO%D鉿'/REſX鋿5x/ɤ/'(}oz;Nza8cƌaTWW #*㦗U7-F qq{ +Rө* `Yiv9г/wN'3X*{.NUU41!3CE t:10ajkkdԩXVӿv fq8,hFLo^X$xڦ/Ǜ TͿ$f`++?]ׯgƍL<,LsΔGJ/#G0?466{mEVtZX:A]GU׍sKGt%TBC}ir(--SO3f؈>i4WhG(W.%ηwG5c m 1֩x뭷~ؿ?6m/jI(̜9ӧhZ߿ sdOe[lOh'////////ɥok2hѢ!&Ef0,J(= z5Po֦:.@UU~9FQB֒ԩSEUUTUX/z`֭[GKK |cleш'YR-+۾_ 2HǓ,})+۾<1%9hr=DE˓ȇ ֭[&77|C 5hnn>SM    pܣi~{CfpQzՀAQV+6 ‘E4 } 9$K_/tLE=\tE}EZ? eܸqdeeQYYngʔ)\.4MCuk㣪*TVV83(((H    $>O=+&P=NAw۵|œ 1S|cjHE~^/999̝;-[{aZ)..fĈahF]]&OĉiiiAAAAAAATB0QFQ^^Nss3TVV^yyyf%yGnn.466       0$xXۋ%#?@l/? C"~,M ?1cM[~o|/m/ߟ4/^ſ?i_ÿ?҈GOw{/IH#=pDEi:AAAAAAAAH Xp4#Vkx[?>:^?=?AAAAAAA!)DE~nщ:AAAAAAAa@xXpC       AAAAAAAAT        B*͊jAQc       z8ug6|`PJ}AAAAAAA!e"p: ˡxd.# ظ͟WOAAAAAAA!anҽo1Y9tF-#:>(訊Jbᰢ F܆ *>i__AAAAAAa #- ӎn]$mΦ0\vTa=WMPӎuI*(,jf۰X-dvv;Y.v+Z +ÍŢhPQU5]'i(h>Bcs+wx_}%      0HE@((bA]8 g3WVBcS+zO-M-^蚮=1zGϏGxOvՂ3́?Fx^Fx'jQqL#ӝNI3˅jÝB5,j(Gz4?hAt=@BV m`D^65 1xhlj{|?@ Lz_       |Ӆ['=-A4`::14gLfRLCk{7ilj  @0<\j UтˡUU;N?}u p- ^U, vtgYdge&'+ ݊bAU5,JA vA-؎o@wk^h]P0ACuXPT5YXXEdA٨R1&vVRֶs  iW}9sUY/*6!a=`l b;1DMĽ0q{&lg1clڐ.JKj_*fYΙTVRm~ysssoԛ7+@'4cGgi= ŕw1xYE;7ƴnD0"zOVZ@s%]2a!FJW3=̾CG9zlj]mEDDDDDDDDDDDDMKA9PYXv&x16EKc$sOPudauq-05:[Ӂ3y] B YS\cpW@G;4%׈6 gSx.݌ UTṼo9h$c1NZ;k]z߃_釿blX< fq@ ;L+gٿ͞Y>'5ҤFZ;I<a=ױFn-ֱgANMlFc #cܰm#I[g<>Z'GibҐeT% |\#BL`}U m֭b˖xbAErAi욵~is6F}(>Jژ iVLUbl{N;(58Z!u _!{Ljupa6<'$KGIӜ}|-l~3 銈\8 ϑS'Ż(A0}k?w. Ǎ%l4q-F?ѤZKJa=1c'16`~fu;OOj,c: r Wi$J)Lj ]4ʂvkO8`{+ky>_yٯ>+!x[y`MV=`&̮SmI+(v<^*#J9vp.4YeyVcΑ;JJvmidi=3 QbhN_Qؽ8>Fyq>|sHA9tj|Np!Z|t@VoXmo7 r3dr.aq9ްCPOH!`{dOoZ'ŀ% 6_U Ws2 ~kLK'"\] yp`R&WyYmcJ>_"Y,S44k9Kj'oݟgf$ǻwxȆZk:kWᩀg"&-3:1Ɔ%H4g}0vb+zs$ sp<:B1eHhBn 5ohx7& %Δdkm/`gatlxGKzrҷ.,&05$.dfz$I À\(X3φe89(K@/[7 ~mcs/0;_!"=P/7,qgZB ڴ PkxXkC<:0f1Z%\1M U9P?>ɤx#$>Prc-^d( %%QM(-t,Oq /_Az;rL<噸?>`:hrŭyX:8gҺ6QVrM狼27KRFH;ք䃕1}X[m8KĹ$֯[:;;`pm"˵iT!\{j.1tzbs:NMN1<%iN3$߭V=@!odxȪ %*cKMBPbޒDJ0ГX19p(eSR58P`( S\NоTWu׶3ԙzڇ vb"OXdwikCx֟ε8fDJՐƂtBGC굔yȵg +4j Kб4f_ZǺ quQ2R7 /񩊈\HGAq\ַodCfb zw_Ksi+m> VP{ xK}6X!Pw 1`h*sxF18nJ]ur~< qW6\wC;szi@[_"QJq-G"ݓj'`lօ9ClS%9|$ސsk!{|+`!v#cM1OӇu`Okˤ)|ڠ}ĴZ䋈țP[H$:T( R` IUP 2AbG@́w '`,ެԕu|0;x>Ϫ1&K1Qrـ7jﳪ}ojk}-yN<]ģqDT\D9*r\J`F#I(YJyg]lܐgpGsR!MgLhs4) ؚaGc>fƐ7`!l}`|l\p4D3 _k'3q~3c|_DDDDDDDDDDDDDηw!=k+_:cS{:o8 _pٶM 28.^pE\s־ͪ1YZxxpq@e:"M#ҜP0@ &l<|~F}Ga] w4$IR.}rKKUH iz5d(EC>H=>IpƒtH|u@ΐp'IbMP0=KC)co )vXncan!a&m`oB!ĚAvW-Hu^E~G{b)]ȯÌ5l8B{*oH _{;;8^bśVDo8Yq8iaôF0ШƤamf8a0V*[e#xDda&簅 OYw8DgƁ i &0+gs<6,6Nq1xL6pcڈTSCe_Ji0z6Lyl`{ODmEK kvvGر⺟gƳ^%!#jʋyz>f&&-=ﺑ5} 7p4M}{sqeǶg_Je _%IR7UxP: (>OwGDGJ=}9|b[;6ﺰS:≟Ʒx?w=s9z fA}~qڹɇh/\P OA9$#OR׹zelLcaH uc!JS YK`;S0x ͧr`<)тan:a'i|g}wdβ\cni` FG-sGC;9JVuCPh(u;ٲ1ORJXd7d/Oh3sCՎ>`<%$ۮ Yvj~ "M-SsMp٥X:eKgْ [] -IVvԂ `YU"/sODTsligx=9k$Dg-b7ל%.#?;oz?z ff拈[G?qz; zc8v 'cη;4·уoϜ:P*s^>_gR.qhOhTۿ}?~-/VC?`7/ޝOg] aޠc_P֘clIG=F1q4;1` [%LvlX„%-bl0&ēU{8ׄOxd%U1ZY:RJh#]#wx^lx#ÿ;o'<ɱ Խ%""""""""""""&2y0'3;Xy'0~ XnA[G7+`MLOMs1lgyG x:{\\ȚmLfb{ ĴZx?DyQ)?O8albGm7\FǦHVQd(i2EHV]Z1tt Y^> m~K IaÆ,4FqeY/ l@WH)o֬S|ಞ`ubnl[LXƄmؠ1y3V\] ٍ| +tG:f),Ak$JQ,XJ:1x60OcIMs1=ܢEDDDDDDDDDDDMmUx׺;D5 A;#aDG{#?0?φWYU7M F}C\GiU=k"Mb\MAyV57&1vltA]l%74e!5+1bYY &6{"7>G3wr}.sx]vɅRR|z8DPXCP#BPƹ8fh4b8&%qPYhx s2B|2]k1SE|Ҥ;O_O@_G@wGHrV~k0Lk { & g V]ukya;I޸X_CP{'\.OZ!lqO^v\b/^yq /:Iff lٴuR;iR[у˪ڃu;,̱0ШSІv\sBNc'7Exl C.KPZ-#,#@؁ Hi"&KKUUiF jfIR$,O *1ZCo7W^.'64\Tx癭D1,Vq`OH!o ƙfVc E4g't]ڞJRZ틈w0'Mbʝwz rγD)k1Kڗ@Nz qڢc.ĄB"n}O8&k_ZKXDضm=Ak'FrEg,UY,Q78IpǥZV`?>19Ko@{Z1Q0P-ճ0}[_mH4f(tl" ְXY;9]._oŞAAEཧوX,xXsxY`OmYRmy1,a6]ȕFLP"JR L͞dn\ZF$ISs/ؿ=K<=]rdd- <+=2DPs%먥C?1ǓO??.uAg 3+_V@WZϙ~2/|\Z;s~x_?_y+S^Oz=꯷Bj&_?{o4/A!=N?b ~2U䟥W*_yW+|/EDDDDDDDDDDDDT^mpy)T~ODDDDDDDDDDDDD ,?b^o0zs[zEDDDDDDDDDDDDZ,} #O#|3sy_)H~iZX(?g^oZ3+ _Z>/""""""""""""" Zb_OEٴk5?A󫙯֯r.W{ tm0k/7׻~2{'Fr6!A9ޙǿg;߳֯*_3+_lӭ>?Ggl?׭_v>/w}NP೟,/{JBX$Iߧ766|ӟf֭/y}kJ%~~0 _v=?8O>$###||{zzZsǾ>q>x&fOwDWWŞgS_r3ϗ/Odo2??$Rk~5 2== Z}oo}O^'MSr+G'dϞ=jg4{G|y=>o6#;%&&y'iżDDDDDDDDDDDDREY83~3RR{{.+_,}1=sssas￟__=G?byy8qcccw}Ð7233ñcS׹;pq1>ǎ#˭V;v{Qr\s z+bqZ4MN8u]ƍ8p?twws-pUWY|g}Aws YwѣG[nak:U';";oy(/oZ6oy| ?T*)E y5A 1_|"$Ogg'\s >(Qq=|du_(,// G?csb-[ȑ#<J%*j{/cǎ1[dA9pZE}Y~r9(h# 7no6R.W(8usssٳ|n/)R={p]wz| _`vv\.GX$MSnW.R.h'^=ş׿{}Sx׾89FA[Ĺ~ygxO[[wuwu1ưo>^>ϳ{npw$ _W Ðw|Sbjj|3/y-[ZFGs<#4uVn`}ffOӌ~v @GG7n䳟9گ}{jNmmm|#׿Νwɷ-n6s|+_! C>O|}k}|X !I`eR ߊs|={ ώ[xa:6.۾ݻvy!0ګw O>,S3yvUW\Ǟ|j0 ^3 ^:Ίi,,T7^KgG8̱',-$)77^KE<|V3{vuy bO l{6m-B^gzzz5߻w/rAvލ0 _$4Հ|:l߾=9˓O>4}{YZZ⡇A@.[m$ ?0g__A੧ZK|ͯxvEET*:SSSR9~8'‰'^vko qjl?O27$B,?qjzAĩIw=>'8|Z}_=4'N}"bffw_=~|#GO#џ,,VHgiFIXY9ϗuqwb~&y*1~䯴G+Y~/5׺'3wqRVO/x=W\AZٻ;M4jPM0)Ɔ`q[^;$_vxOV\ޡ5k+.&ZeXzGR|\$iBF:]jmm 9Yzb2U\ҏKmݾG[iSu¹KP^Ssgϸp ?%=JTPך(w56v]y)Z*//׾}ҢE/^TBBOLAIII墢" OAAtj޼y ٳu;vLVUSN$\A/]%nXv96UWWӓ'ccc==,Ygڻ{j?Rw|r2uFGi|Z&dcZ0VuttZtYn4~\;>OUS]3]\FUV(;s]PKKfNW[[U%셋fSPP9LT 73C0PZCP0+<<\)))WWWor8ڵk̙UVM?{ҿ"""BgVQQ>cl6edd{,$@[lR[[ϟ/ۭ hǎiӦvfy)**Ҝ9sr|LHHPssJJJTRR"ѨPO?/SGGf=z-::Z:r֬Y#ŢvEDD(..[(\!/I$Ie.('+CE5cdIR<TYUf{\.2nv~6O'US['IJ6VI]WKoQ7o[V=}v~kJםoSڸAp!86Pvz}ۡg_vkܹ>5yd>}ZN}~39sF?IRLL^|E筯!ohʕڿ^{5O>+Vxzotaa6$%t9)Scc L$f+$ت/blVzZ$pamپGKӌd ҆۵l˰qZw75}>tgTn ۛܬt(֦mʯsO}Uٙ̀DUCMPl6|>}edd'?vd0Om6yuvvBmmmUDDD3'=d0s'TUU1cxYo@ jjjRDD"""$Iz饗ԤjxOCNN~mGDDp8TUU+44{H>QAnv6K)5%s_zɤD)+#M&S F͛3SfsvxxO!!${KBCCfN׮_ٮ[)OMtK[l6r~}L57!I/IjjnļAŜ }yX.KS&*,̦ZeS7{KPrbVNRxmPE>PjCw|zQ`4IIJ-4DM.)1^I*X O:{\;j/$).vn&}y $f7Ra:;0Ofz7@W|3m>Fe˲ަ y&eWyJIGii233=#qPQ>N,9_RR_Sݝ(Ԅ+jhl`P-Tu:;jhlRxso+\.j/+((HQ2|\nh4*"{D=W$:vW ZN=Y,D[l6)f^hKPQW{ ar%a靸Js굶?w{onפew㽵;w{މse>fxV##?F~2zp_=?2!y_JFvzoÍ{wo? {r΁@cOGH&No;݇j~WwGpdw`uXpDwP]6-nf$d2hwךhh@5< >744 { )!!AϟWmmCnH&I)));d24~x1>D>D>D>D>D>D>D>D>D>D>D>D>D>D>Fٳg;`HnDq?9QV!ah}|M1>ۙ3g0h$rrr0h @!@!@!@!@!@!@!@!@!@!@!q\pL=߿__|;wN}y߹se2%K())gu֩M˗/Oݮ7xC .ĉ%IVReee˗/WBBpt dU]]ݣCUUUr:u֩PҥKޮB\R?mo޼Yt钧Naaa>}O݈*Xiiݫ+Vhʔ)lZveZ%IMMM ӦM| *,,TXXZZZ|@FW\\hM<Ǻ IgϞv-\P:y|JIIQvvZ[[G$v@"?@7)dT|| Ccbbd4UUUlr)..N3fە-ݮB=*))Z_N8Grra)e4Sv%{NU02 r:2?$$Ds //رcUVV{Y **Jyyy>g3 IDATD~2͚7oOYII =qqq:|t*..N&I Vdd&Mm۶T{$jz[_~97FlUVVǺݻwj*55UfY4w\+((H'N$Y,|QlĉѻᆱK*33Smmm***Ү]tRl6IR{{d$%%%oWbb<ڪJP_MD(裏j˖-)I ׊+4eO L|ʗ,YӖlTGG'_\\bzz%nw bsײdeqoS׼cײɫtΤ hb.KuuuX,\-x9QV4ab0MR$$yoëëcW?cWۤZ>{D=W FƎ08p0RH@H@H@H@H@H@H@Ln.K5t;0 e U\l !#vLl,_00ásut2d42*̀O}0kd RRb*Ȉpfg9(pk'mUTW/)Hǥe(.fv.S Vm۹WGԅjb^{~IRm].U_[ Dk{{p۰eΔoF9' OEmmm#Н=_ԑd5&*JK-ӦI7ٷ  $ @˃ f{;;;UQU͕nzt^%Am>,>Z-ѡ+wt;+91^m:rBCB*=_G[c"U[W5wZZ?}n~4='+$A+5&*Btq57ۇm@"XB|~&%%)7+:}y yYJԕ?qj/թMᚖ?Iui`PvFR}ڬTCGS96.FPP&M,gg (9)sUY]3Ԭ R0Y]?.=3e Z41/Gc|ڮohԑWhS'b1KN.Uə &k.rTO{GҥŎ2 mЩR0[ LTdD;Xaݿ"ScZv.W"ɤLjljVB|&OȕuLuL r8JJׄ,=qJ+Ir< |8!7KQ_ WTUuvt*.e;uL:]r$u}讲Z`T~7 ΔSátO_RTERm]3vgh='446؉74*44D1/k?<Aw.VM_k$Iݾ54j_AIRrbuBOV[[DiI ZfM@"XVxM˟P+*2Bn#[65u)EFd}ϾqɪѯZ4lWt@YrKi)jim?ݤzTy9NZqjo[Շj֌~804Q]ٖVI/xOARޮ)s%Iom4.%I Mg[x KΔoAfgt9(,Iϴ&elvշUHmزCܧIp8TUVG/yPq@2 JUHKD~b͞9'@>TѨ5 [d6mܺSL+Iڱ ]tnmزCVE%jo~}sP:afz"m:p>^Yͧ#I:tz%&+2"\[0Zii=tʼnSgޡֶ6}&-o͙)i|w[x@v{kɯ.S(8|TNib^{N8~|gdr2t^.\Կ~oܣYNN)SdD{ѿ+W]e-ԣNcc $ORb,u@>ÅD>;ܦ3eϟbgO?Y~־){5&*R C?Q֮Eɔ֪>}K$}S-ctѩ6.^WΙ5%gϗk]zDoWN> ceGll[vګY&'t?أ.νEZ|\ͻmOl}R[z.VV/M_O5JJ@ʏΜy.k ]{Zvbqq1z咤m;j'OKL&55E9vJӧN]CӥkڱP9N}vfMG# a<x+[? Lf)9)μfy޻?t6lޡysfްF9t5딕',N.+z[2_rП?2K6mۥlC46z"*>vRsfMmN-4S'-5Y}Q͙J]}G?әs@  _~~/yNgϗ r\2LjlVTd"#)KMIԉSg$IUW($].555v٩&Y-V=R͉Sgd24-$d Ҍ航ke2TS[{/IK]CBy.WH"tJMbѴItJr>ߵWkQsoEc"% ;6:JVw{ e?$zĄۤ&LyrjJN=ns Ii)Ir֏TxOj^r0(Ȩi)$ISCcnSOF1.5Y?WڵH?yWZ6ObiN0Z 5TФ?%g46:ʓȖFG?%i9ʟ]{m^mݱGO?3ԱTsNVE&IMN>ÅD>a6}>;%>~A;jR^Vj/)U4fϲr{"pQ5v݊p3 Z5mt}yoܦyٽz❝CgF9Cw;Vu36Zs1Ox/{ޮz utv(+c\.$ws0\H(11/[3 n\.m].$IWNٷ\{p4}!mwSCc^NvQF1c};&RzŽuo6u¹?wV})R4iB&MсCGk$IƌW+zKtz!Z dkh^Ӿڽʈz\06+c'{=nAgNTQUk"`; *lP>rbPuG9!.v NQ\+K>FVO҇>U[[L&:;}JJUY]QhH$Sm# s0F_oo".!ș?h4j2I,22Bru}yXmmjljVkEADk㧴uMҘHEFkb^aΞ áKujдe1?kt9uv:TY]vI]Cwtt`0*.fl] WVը.ۭcd4f }ޢZ\.Xis$]jlj֫~[gϗS :x<۵l6L{ :::r}k**PzmزCw1XQ%˥Q2*[VmںS--jiiU_1焐`-VZmܺSoIh3gt1_:?9N;_=kLT;3JDKkj/)88XntDNWgm_7}} WkΝdRrr,Y+=7ꩧRdz7d%&&7~XXyv;jhhTXX4g%%% y?˖-Sjj[o-\٩+WjQn,!!zz=e˗ݭ}ˏe2pTo=T?DH.W׏w-{/ BCS귫?_X)IG^K륕ov`(-5Y_1c7%u 5%%ZZ[e44v-.i[x@FAVUfLUq-;wAJd4;%몽ֳz+{C̓˕61ޮ||ٽZvV}[NRp3pcjmmpݧ3)IZRU]~/l*3#M]t|<+ӦGלYΪ鷫J2ʟ{q8}~a+U c^'c/n{M^ݧ+u&~oPG#[N?233ޮB=zT?Ң뮻tlsNmذAwޮǚ?z٬ &A?h"%%%EPii{9EGGi?O?233={OzoV~n[u T\XGޚ2FXCMMv&l 2릿}}`0 &XSUU屫۫mry-w=@+Piiݫ+Vhʔ)lZv,:tGUpp/>Y^xAEEEzoRRmv*/A{5c w})bQLL@i)H1c kh"#]o6olHHBB|FAa7dR~IVEI}^Oq>zFb`l^ې}w뿟tNm}*˥А`ӥg+.4$s`0 m۶'`3fggDv]'NНw)Ţ}2rp?p#(;;[*--nݲZJMM(==]*,,Tff߈h…JOOתU9vwV||%I:r䈎;T\~` &N:|Z[[UWW7j׮]Zxl66gֱctQ͞=vUYY3uttZwٲejjjҶmۆ7--ME۷o=dgg+88X{Qvv7P>l٢>sU|xxVX)SHFG}ԣg՘1czGDDhڴifΜAg?~L&222t){FM6M۶m~F`pW0 ދ}̍^ޓecǽMA^M^&ӕ:Z[[7\.dXpQDYsNQvf/t1 &XSUU屫۫mry-w=@+hرc`F ||An[V5^5uĜ9s̙3gΜ9s̙3g3WM]h0 ދ}̍^ޓecǽMA^M^&ӕ:Z[[7sRiC'K(+c[lb6VGCm թӥLwX~g0MR$$yoëëcW?cWۤZ>{Dipm:E% ::;FC1:ps!#w |9 IDAT||||||g;~cwq;~r!Cvi~x\<"{k$ĉ}*ȺSF= 0\.F}p: T֗镏ZNPNZf$CzyEF9bD~@ۿvܩjL&%''kɒ%JJJԱzw ө0kΜ9JNNիUQQkZbVZquڸq;zEFF*;;[/VHHުUۧrYF<Ң]v㪨PhhrrrQ>b4}¬˘Mfؔ?NeO{\Jৈ.$ԺuTXXkҥjooWaaV\\yyy$ásiѢEJJJRKK<_z5uTeggKv%˥sJ'!_WW'w{{~vkѢESuu6mڤ'N[BCC=:tHaaa=mtttJ.KvZ]pAK.UTTT__$nJ /o>%$[h4*=9K!P~D~*--޽{b M2S_k׮UzzO=55U|ڷoVXs\.M:oذAze$IIIIы/O?T>~\\6lؠeee3g4k,xۥ?|^Iq)&%$t蕏Z2wK$ɓ'X`555ٳ}no4֫Ç5sLO͜9S>;zj544fTT\ 6BiC6)6Eju R2 =h4mnN82 }/.ݮ^'$$GB~JNNxtR_Ԛ5kt!v`~xd4OѨm?:D t:a4e0t:}?#K;!`tkikk|U5gmm۱a\P0Y[{6nݩS[QyE_PSsÓ$^ɒRKO9sbbzQ:s0F 8?ur:=1?e%&&f)..ǰ`ddRTTTu6M>}]s==[V͚5K3g֭[i&)55b0\n55; 77F^2Mo=Lj]񱓪oo)5zoPD}$_(ֶ{u݋ Ţf55p8ti&&')(Ȩ_R Z-C)ɉZwՇSZj-oW}C\."ysf*5eRF/j{ϻmfϜv>1_Ԟz岅{Q0O P6=pbs.kTx?w If媷7-6 cDpE~VeeJKK{۽{VkwFF&ON_/Bçtj߾}sĀ\kÆ }o04c IQ^Q yYZz"-{kCB7%}8t'JSu7?~bTv~U[W'I:qOc'JꫢJՊ%ӥjwtHJ򗝻9чj|9NZw FsK.΅s}i)mX5t9j9# VUn1l\[W;sFgg*{"hĉѻᆱK*33Smmm***Ү]tkN;>{PI 1678ݛ7f7~ISN 0 0j@*HѨf4s y9=gߙ9b>߲\o?I˗/Wlls?yWڞ={l6ڵK7nZڬ*Tdx׾3u rI;QRXt:YmܬtY;l$&+.:RƔrBI1 yrEG)2|x4n&)7;sr]QuM:l WFzM-*֜<=ۭ*9UYv<*IJJSbB߀ ڲ}R4yPਵ]NjJݣTee gUYEڭ69NDiԜcJKIRtTJ*5ov{v;^6F{M5u?rܞ2WVљ::d1А;Q,kMq1#0%2 )Syez{{3%ゟCcS=OsHR_KSFz"#<SrRb_εb6:}^=>L˓?Ϲ'9)A[v`O+{@'<sgz>3(6&Z{U&);s$sQ:MNKQ~ ~~d6i!]]:tڭ P^ncGݪR+&*Rfx}6r8LWZo(ﯶv֬ߤeU:U^?MVTFNΨbQzjD7>.F6mUbBrF~8}:zH ͊TA~, JJҪM U^Nü l^^<#0N+Wj۶mZz$)44T=O&MgyF֭Ӌ/(t=ӊ񁁁Z|VZ%IaIhTbb%'\Y'+~NDF($8PkS{7أ;nYYfv$)(0@Nj˴saRR|zlaX~&$9Ejli gKNmغKߺoU\nNt1{ZAɉ:]}FknQ˗$ۭ[ؘ($I===AqC!I2 @~Ii^uh۵d9>mOBVݠ^ WiCvp*-%IGId7i_{LFڮ6olQB|e.͝5]wǘWVў?o22S.?WήnIR\w`d2j{^5;Jo;k3\{x>sM.ksA|IJMNPݮFGnW2DE"^]Sxc/fr90_,Y(IZSmPK͓;7hJd=>f:z\F3*#=Uۗ|fy߿}[Hv;nԽw&??˘Ϙ7oWM]cLQkٞ{4cZ?UV@%' ?\+hƴRZJbΎu}cwtsfӛ>лk6?gԗgo֛nPyexgyCo޶S-Rn?jӖϴynEϥ9^3RG"r$.K-uc3T:W,:ѨaWbQllA R||9KJe6=SØ&͙9MmV546{/_`#ޭf}cs\Yqv:IPqXɨd3Wy13nUԤx4V_< eYC!=v$]G~g~$7M^? B#')6:B'JrTvW~`0($8XqZp=*-v\9~~JSGM4`uF**p:k9GRp8uοn28k9PYel]**.̂Ԝ)wNWv+;s\Wߠ1\,nygI}Yk*R}C[Z5d̜>*oOz\.gWUIiJJeҌiyX{P{M>nu*"|+I1gE7bkwjkhgu &:JHK5Ռ4T/~:ah}qb"#ҊLӶ OYZ.*2\Qa|SshڰSHUzBoQw)-!cy.K5ժk?%)+OJbzw{gfl6{*?%>S6O )8(P fUWW>xv͚˒E:pRfxc"TuVM-y!*uDM-zo@p2t˓:^T"өyo\8W5ZS՛?>6ZAAQWur8Î 0`,Z>MsEE`Ϝo0sTY,fffL@jzFQ.ZZܫUVQZ2Rg`̹ٙ =}V[ZUZV7o׋iSs`huh4Oe$k'aVtTFB6Iy9Y^WWO]ۨ-7;sHSmT}cKprvsb}RYx/ʪ3ں}m۹GexQy!)1yzˬEtB]ݝr8r8 ,'GK؛JɛbKkk~mڱW$Y8޽5ןFRGPW$irZڦ)sg~'I3k=}y^~dDx|a\|cMjljKy`eںaenXћg~ARFRnZ<_{k2/zBCh"d2uZ͝u>^QY}h t@aTEeyRSgS&egN֗'Oi `{^;pDuvz+q >ǾbW>ט \vtv+$8xHہuw("[otn.p-ԷϥvMtiϬS IDATkכ  g%'uv=9 5kiAΦk #0L&"†(I&QaW9ݣE'aSqiݿ5(((PAA] !rQIi{t29/6Hγ9:zB 8xz~F><<\ٚ:u-[%Kh߾},-[gZܦVIb͚5k֬Yf͚5k֬YZ74]nnp? ak큋qm-/wwwݨ> QXX;v~)+ޮ_z'9d /xs=?\|~ӟ* V %*IbreejR,fy^n:jRHJ*4%sD ` g%'uv=9 5kiAΦk 3zWҺGGh4*##+E555^i&I^ic{q>}Z===Zʕ+/Fd6+|ןX,ZdWکSt4٬ \kΜ9 Txxbbb.\.w_qQQQ*((b"s\J3^=[ZZٶX,jekn?ۿl[kbb~R>n3xOdam=p1mz4`=yy@\էSRY.N*ה] p ~YI%Ir]8qHw u={@{Т=x-E W||||||||||||||||||||||||||||||||||y vpOc{GT#j459)K)T}[U,)0|ۥHɊNqщNwܮq,!pa\WnQNi c>61&Y:.2%F@>c+62h4*.2^;?%F@>v'T^{B1||TxkO\cC pt?VmCDeL\.E|Y__Dvb\^Nt1.[ݮ-wg~AOܫz?/a\N.>\MnY,~|'{o,06.r5>wH_V`4hrJfg+9>v/7P]C,M~CA]4 p˓jmk׍ z7unmkפ.e;tv˿xjSsTwuuÍ[uq Ffgj¹HOkmkTqi,Re2WVZJn p8߽;oEI Kߩ$:k鶴*˥0FGjɢyJIN* I_r<-7kvǨGa}_y35g *ݕk4)4XqWzՙZj~Q\Ϯr9e1[.x"g z0YmoMz';ݞ( @_[<_?_{=S''X؉bb`_(V\͝]XEF:5{~XOY?NƭKѪ<]^{Gͭ$?E%e^,97Y5r@]}>e䯬:Egkwi9}^ևh $ٺڦ[o^쵤%OtѮ([W*tzجk4i{!3r#Mf9K>tl&UKUU5r: wA>Ғ=i'Jʔ9N>S6M=vB.kMյԤxEGJҘ\.V()>FB=_TZ(%'(-9xn^Y^ M-=~&֔thM+uF5uu*0_JJw75ZsfNNݪSU/Ijmj8%&Ie. hQk[GHϪJ[mr:92LӁǔ{Uja:vXͭm\}~er=e:35uuvb6+7'S!!:vXb4-/GFaKJe0MWVW9S2.946(0d8$t1e(*2“~1%'%(;k&{җ/I? :SSFkl>vvuiνzl}*'s_rRl+ПVN|yR |/gjgQlLڭ~FiI hzu)|klC;t [;,ǎwX+߬)ch[tْ$Sҷ[6.?'RuwTy'p+շV`0(91^hOt-7jқr{$I'Tg'(nv/$IF/)-ޫߴUy,'I*-K+???EEiq٦'y@ٙܟm]Zqk뛕Yz5&=h&~T[ߨ4mݱ[[mI/2'iRh+ꯟ~Bq1>Qј:^tm0 =qrik~MKڭ6᭵c>iRH:l]zϫu筋{nѢ8i:qȯW`1{mV j_W M7/"]͟[,֎]TZjbu˓JL'_9Kn[,$6Z%r4%s[yf=^r*Iw}PR@*eNNu߼P.[G?P`@~Ҙϳ`,O}W|I}ÍgSWb|zvURofLӁC_x*$ymͺa\ 9\Khƴ6F⛞_ןa{Rzjuֻo$p6|mBqezA#mѧ;vO1g>f~zF}x~~F}oWVw'=dm;ڮ/>I6mL+>ߨVDx\_Pe>Sϋq ?)9.n\.[tgƩtōXFCnṂx$!A mQ ?ggC-V+-9'fw]ߢOח,M7.PepDg4h,&2gLcT^Q8Z驲uvnWsK[Z5{FySɲaON[_^y=u1K`1W\ZɊot6)T*.k2yJ=/ff֭gLQ53q(0:nUۏf7|s\(d$FGDI\.Np8jYbͣwgьyZ&}T[&X=??%ǩ&IU0U M:]]Sr8^ubϏb28k9p8zb--|F UO>9IbQ߀?7/HXY3ur:VT)Y政QJJ燿҉/KsW {xL{R~=J勇Q|ٗ*:TT\S秩9SQVv1&u Jq/X̺Ş_[$IZfJ MjniՐ1Ϛ3sC*ҿ?rܲ]uV%*)-K3MhcC6mlW֩I{$Ŝ}ut߈a˗ߩ[wk9d4(Fh -3֗y!FQjTT3jTPW2"쐺*(&2b<*{+i3'thZ^gu_)?1w,҅$%(1>V6'-7'K iJUB\4wtj)=5I#vXe*-+qriZqZ8o$yTy֑/[+7;Sidsh¹W+ovWCaB%"I1QO3DTD$ߩ)3s6l+=:*Re^i*Gk4Կ]JKqRMcj\ZJ"(0@9i:rDNJOtnʓ{>{#u7i2d^eg?/æ%eZ0k콽:eu~u `DjoOڴ3+!.fv]<]6[dk֌|[,ҁtؗ73jljQWww΋ VA~vޯcE%jlj{k?%򇓕_9N\~¹M]  Ҏ½ovdfiڏt)?o%+*2\T}|Cӧb154cZ4ڮқoTwOZ֮!kzFY.ZZܫUVQ[2Rg`͹ٙ =5X65Roޮ_ySӦhܙDkhJ*#IrC}}INnEGEh4*$8Haa{uqeY0r3 Y0UzF76T */'Ka=/o,(y9:wjж{TVyZ9}gZ+:XOk+)!ӡN}r.6@|Ydצ{qI`rwrJ#laIC{YZF]IojBr2EW|Fe{K=q1r!_%k?@<𳣯Hd%'ƫM9SO.'N4gt}{.~ zzoɈ0= "қԢ^?\ i'G IDAT'۵u{\FӋb[ݣ7W~72 rܴx ye6_&h9z Ed2 f';|pLmnwxcZy]־G4 KkiPDGJǿ<)3Uj)rf3'˓4{ FQfh]s0 כ>? @lL^qWz˵jGf>Ͽm;h߁#2 93*Iwi՚wo #rr5sRAAzzsZ//I9}ۼ3OUkk>޲C7}*٤nY9Vϋ K?>JKhURyBPMN;?Y2>|}1 #.a^66H˓w4\wJ*U6Sr ^/duϢ!\uvbQߘ\XI.OͦNM {stQD!y\.UL__2dS=ezzQpP`0($8xعۭ2H bsԏVCS{Utz5^Wyi|WZ}N}sŨ5+{o\}1\\vtv+$8xHہuw("_!X2q !xA>B!xA>B!xA>B!xA>B!xA>B!xA>B!xA>B!xA>,wpmٲ%Ǎ oҤI.8n !xA>B!xA>B!xA>B!xA>B!xA>B!xA>B!xA>B!A떻Ihמ}ڷE]=Ţ?v;Q@@~&xP>ܥ{~wtP0 UW[:UUU43A>T*_X7oS PC}2ٜ._ZBxLey.ė_=Blۖmyҍ^ښrp#8ji]ݨ U P@K$;n.gtެ>+q՛Ljmzu[ڲ}RUeEK8YZ;Т;oA\xucF5^p^@aVlmS:U0% J; jmP8ijy3k>mٶS,LP/ Z]͜6EǍ$$ivm۹[@@'/wT[S/@&׏~xE}թUSo2% Bڮ5֫'Ɔ:]vlPK[2lQsS^@{[PK.ĀcӾ-E#6ef͘ZZmJg2mG uZ0_$&SjiP$iZzyٯ >;ȕ+oTOoRdێxE}&y-]R_=lS6oҏ7"1O֫-0tM~=’6G+/`Zj~_SRq$1=2MShD[R08p<#CM O}wݪ榆BӋ_QSCO6meYW>ʓqcF{Ϸkq2yLXZbZR?تh)zsS{Gi۫[o({т%IHO<>wgtI*4oLIKTGg*]W_[KS5mʤAӫUkt7[^G>vfHĆ;vQ*ѼfT/I5Uw[+0>U=>[2<ΨKʿW{~e9]`ٜF77jQF1yRgb(-8d6in~h\I}֡^iv-~y:CǕ$wtI~ȓGhE P(.MdL0`ߚ*ժ먟%֏F#zswů?M\ =1NA>ƆzܽWLV~0Xl X,:6~Orb]^N7o_=s]Wo٦@ Ɔ:e9jzw">S71~o>O7  {8aYy>w,I=cO?~A|GG/ߧN56x4P_-wu? w+45v̨>f0_~OU#P#dɚ3{VYd*sgVtFVsg83J3WHN(IJӚ5cNx,gD ;.Kc^y560-ڳ_TH ydJ7me֦-۵g͜vRIRSC~^Fk=p~qcGkZ99"NM4A}-sӵ^eW6◗*K.8d9mxƎnV$֎]{eێ*+FA>C+ƽeoBNx_v2-]RK%Ijj׬S^\Fih4Rێ]Z;utMhTs竮ОǞ$UT$XxO\ڮ"{?{+- >{ ,7uSsfO׽-ÒK;ftߝCN50-~yLӒ$UV$4,]OUP|tdľ!y?`@_[~6sܷj8lݪǔA%)ETW[@g;qG]= :bt&|>x,x<6`aJRe*BQmGTJi)(H :ǽWUeq|㺮:z?cضt:#۱UW[3l#i$[<귍o3k(nea-W?=FPO|(JUUU˱~\_b;~}  ;@ښcnT_W{5ϧOw@[wÎ !C|< !C|< !C|< !C|< !C|< !C|< !C|<  8vYF7mڴr7C|<$X8uݣ-m몾V$IW|A>OUUHď8Jg2 BF#G 2LSDgT:PpkuiZJ [Gfإ?{,_}˪?>ҋE)J2٬K.<_uZRzo?*+N݇: 8V|S@`{j֭ߨ4~ܘg|4λԓϽ [4~h!?ЪZR1t?vnJ|>%Si}q-_{=0dRi٭ע͞nFIR&q NI-~ij?*Һ-7{U9}J|^k֮{4mʤLЛJK._K/P(4`@ ۶[rl[ئ`M*)^PP몭S|^͍ Q8Z;3I,K]=J3 BjnlP PW(R8E}:qOG:w}#NfϘvh$VWYgCSwA*]~) ._DB^Pbӯ{Z3O-_dJK'}QƏQM?*+խ{I^@͞p(7^U)4=y\ƌShDWV|>nJIRGW,ҟ>xjkTS]h4S'k]mt-*fL\?l׷_dlۖ$55 㤇??^7Z D J@jk?9*𫣫 IMJ*+ŢQ$iUC}gL;Wo|WZ4e򄣞k̩ M>jS}]Q!N ڹ{l. ~457g*+jnׄs i v}l%IhTtKG7j F$Ihg9 ::o8f"?y$ɲC!Y}l.'05~#׷E1\[QwzÇ 8Mդ胏>ւyszrG&) g]]݊8r~&3F=Xcke BXTPP uO8o;޻v).;W,H6md۶ϝue֮tۣ2BK!DOx^tVN/\'a9si˶:ҦѣJ[Z۵k~M3uQ/I~Ov IDAT^g_Xsg {áP>OƏն@ƍuJj4a6mٮvjj$M[+ʹs'iܘQz5VcC#9b ֖x %t zcҭ7^ή}?גeoi㦭]*ɜ9o*Ihڴe\翕rśZΘ_ P((Xv/ܭ[ovp8l_|I̍~)m.}q^Ygxm7_+ߧ_Ip6mٮ}J]=E)5W^={kMhaǏ8]=>3&#z\h=YO/~Y,}sZTH9Ǐ?ڗ3/_pp7f}o1ZOo8pƄ_8ryI* EB"7C mG_Ə_~A=KzJU$Եnn_S?[/֝8`1w ֨GΟkÉ{ |gV(Mp |`~fv.q] g-[5wr)Q(jmk|>TӇLF]=9p| C]==b;N'(J+/oY㟫^}a|ߑA-{as{k2ß#G>pD:ga=fE"AE"an>OVUe dsJzsŻWU!hz$Ix\~n575A>@sfM_~A575*A>@PH'/w/wxA>E\K3hg|z0B;kv={|e;?jo v^^'}lp ٥?$˲r~ur^)v֎]{vigl۩wl{` S4srg{?{KK8aTZ]ݧl{'|8?_Z[F-q7_$鱧k}Jg?nZR}eLU_k]/ߥzIA/\.//enr-Zx+*yI`XXVz_=jnnԷ-Y~n wůKaie >g2YZN_$I\NG/ߧ*ܽW˖Ԏ]{GרFi=xvڣX4*ƆAI^tΟ3S_ήnE#?gn:A_0oZ֬].M8gO"aL7:,~r%56i}:V>m7]ΕiY<2Fjn:\?*˴JwtVR1j_}ONih7WYUu5z}p^N/.y]_}Λ9MlV>O= ޻_?Q0T8}ѥֶ)vq*g{nSOoJu55_IŢW>/ӴYPm$UVK V;?Jwvy\'3ݾS/.yCTWWdj.6 a ;o-JccNm޺C/jKX5EɔLӔ'UKzZ]se5c$iy3|To|O5.[2#@J_}nK&S* F"7&!I:ڦ={?_J4i8J:_`m$A3~eǍQ6SC}L[KA۷uvLS UV$&|f]ݚ2y D-߯{RO=SuשF=t8utuk8>Jzh%Pwwj+ݧ䛦5d[=VO2X4ޤ뇬o GVMMn*7sQ8T$d'_EB(_( X9~x\~nݷL6_>$*+'>G#au4A_z|ҫoqbQ ~\Usc"~}{S?.A=T[=񜱚5}U~_W\zj~ȓ?}Wx\t4`ۏ7V<ⱘe-Иw8=?ogA{=#~8\.8[vj e;OoRPPxr* UWU 9NK))dPl6'۱UYQqılVo2h$xB5U'_<SX#} P]mI^ T_wr`Wm˫ҞGEchmu$(#}(RmM1kpz{0R|< !C|< !C|8\ grq>?js XL⺮|>|>߀ep&:jN5Xgngw0?X;{ݓT<S2.se0*+T[]ښ %Xgr[`]=J27YpXc\ /uU0 lS]M)?'m,I9Y-rguӫ F"pF|F"ܠ;/m,EY0ⱘ"pS.+<&hg70q]W*JKY)JڿS6h$Y-0 \ו#\pV:6T{.p6ٌ=`KN][H E=`?WFpC!%7A4߫;CJ(we%-.[;=.cPwܪ唻#bng G6g~ {q\%mzBhvteNɱ?6ݽ.cPߣ?>X2À "Wz[҇zw)`g,C*9lA9ٮ[rJlUOԾ޼$WRv+eKَm]ieM[Ce奖v"{'҆J mծz\]0qt0Wwΐ㡺o;o`*/ #C^/J1%בlCʶJ{ߔ6?Y 5m,% y-[=ySYVw,w9؎#[̍+ufM []YCPoϥ-\JW km9S)Rwxݼ7o*mX-Xʚv kJނ~G^kRz_``*g_șzr Q{ď0vԖ˕Ԟ.rLrՒv\ud CNi׫3ȍSra#Sa9Ι {r~`)U΍,ҡߦi;K;|\a$مS6Hϛr^ =M9|#t\ r-mv#%g *XCwd\홂  sdTcA~i*o j .*ƌl=H,% rNO/o؎<[־Q2Y ISyٮPCRuZ[[ v՝35?0wbudRc' zũ=t WLo|5;T=Qx}Y/e{Lڼ6~_|R[;A:*v\g Z?(MC \Jc.kmiC}C'ʙa܋=SFqh|A>x{)IuPΟPz)wHo*ky[;eX}~M97Be7otud J2w%udۮLA+ue -g&i$F|2G-(~oUG֐i;ɛqսAm=Sd -[)V`#_O~K i_%)IUitKKLO/؎:ձ2VU썟*X^VP*_T^T#myGWF:˕ԝ53mUFOPN$muVJnIԪp}\Im4j+)1lcG;2Ԗ+u1SP/Q"29+hM89t^5-OyݛL-)I#c X֙5eCG?B,UK:?`Μ5|@jlזS Kk:?`ނqvQ|ReaIrniRoKiߦ+'1]b$8xw8XoR`)m)]RoZ|^|;w,G}7;|C]mUk*/וB,Qa7Ε.9hfRBVf sTAVRێ]*YT$SL_M}:3 S;f KF)g3[&#<;W{TEDMeM=w۪5:&*_n8~"Xih}W ˥YCOk|2#q7jXjMؚ\7WPw5S 'U  We;R.er QS"h81n0)~ѐ}^N_1Wʨg޼a)x>kJLUGBR/.B!OsFU)/Z5r]W5W) wuٮ\ VV:WP\B0סMkTM42}}(k&e9{䏯1Vڰԓ/RYkBM8F<|qԓ/=*QVq/W|vC"sW%8O,UF:>!WaM)k|u3-uL~\Od:ue3ciFC⡀KYÒY[75TSSQZߴufM&U *cfW7ȰJ6zW~g_ScEDyV2oYH k|up [jaŠ}7wʰ6m~B;ZʘD(QU*؎r校H1lGb0^ klUT_)VδV8Ι2mG5ѐjſ$% ʷS-ݒ\5'QMI@L+|ʙ*X*AƊϧtRva]ٽ^|c4\CJJݟC QP>jLD:c%">J̲8y!ق hP*AUGB*Xö;g(gXU ) 6ijϔoi+mX}U1Q 2[Ry9e-7Û>5WD T =d5Wbo抈B~aE~A磷S]+5WLP=iNe<6f dW^ӡQZ5m:=ϦZ$E#wٚܣ_$\B$M$!B!B!$AB|G=]&p,ZnՎ$QQv-:g,6e]{Ofot}i%.Z$F~2;|a-`=t7UȫiRv1 0-ٍ޺m'*FU՚IJ5ks4!M&Yyj"Ekb({{vi>d vUCw-gv-EU#8k!Y4ߡ!JIw5B!B!BG|!9 }1iYA~\T\k[Aߙ^ݺ,ȟ`Rk$*''EAR`(BP ʂٍ20b5tύtId}˧c*x ;k,w ӓA~Y|qDŘ6k[(͂sc?(kMϳlk_8ߏxO_2J^z;7;= mfُy2\ B!B! _!F⌬z-ǢZt=F~?ɉkYɊP5;Qzed%|ԳFUCMQM͖b7$'S,7ZTg/ʌ1Y oXJ): #WY#(ȷ deGLU|k7 -oɫ(R֯gF~wfZ⢢`~+}&Yilo6|neًnWuɓ]~|%B!B!H/_ZA5]"g|&1WKk ä 9>Z:ٛfW.5`;fz4fa?LrlCrAzeO ːW5Qތ֟~ IDATg5q-QSvG(er2x+8U|gz6Z߶h鬑[2Hvx8˰i=#'L3^?)jMoQ4}řtO&L!൬P7g{ӳ,4{shI6`{ۓ{$[u]B!B!B!^B5 +;n3۷Mڎ0J+bTZrLı \f]IQ!/fL 2X,,àXJ1JK⢼V~Uk$.u=$9֯jMWLhFJq<-+yEylZ@^Mb(k;A4+$E(-j+m"tLJ3KgZW=5LÚG`w1{QFYtgZ3Jj޾jٍtbUG{3M\ˤ[U^t/yl`7JܝZ&k᳐Yˁm6ѧY8/m6̂L$;hYX>j$t,^WQF(hyXƳ-yM<**iN{W(e8-zLA5=> oU';8;ڏho oM4X*ٛf]|h)h4U]v B!B!BC|!؉2fٷg=Io6Zwz5Ӳbi6k<$q^&K}疡l(;3lQQ2H1 IӦLiYgX191JcA~AKl8 h9=<+$)D4Sj?۴(//.[X}BgPTlHY aoQ՚5I׳Iˊ/n􈶳&^3i8vىF8t%Q>7Iˈk[B!B!BW'AB|M&=߹F~NZn;hY#nDEE8Q؍)-5ԂVP ӂ}:/6e2>O˘)az n QA~>:nBi>b%ͥP?e,!tpM2/.MsҚ=?_lu?fyolb9ؤ d~ϝqȯgk},<1w; +cGeMB!B!B!^ kjI֚U~ȷ M5ԆyQ xMXhh|~5 "M޳Vi(I32W&Kygf0-H kqZDz<6Ii^R՚80 O.F\5es4}wGXy<'8û?=KvIaRj֦ٝ5f¡6xU]ZW<~V{+?`[÷[dbk:t7jTF>ȯl7ʘ+I!B!B!#AB\1a0kgI˱N4- ṭuqVլ>d"t,B"-kiA}f~E'R~QiYIqJvhQrg%ִ]Ժ<ˤ(Ӽd$ńQmNhyUq-B8vwPD#3_ti9]2bԺi+ 6uV/YAQC\y]6Zoh;]BCZN;/C{ә7_i؍ң [nwE_'Rr'D!B!B!8Bq4hZkVCCe8G#/+ދ2ЄӌUd%YՌ?>V0b*؏2LQVYˁs|2qQ{&E αMR]!-&Y^4e)ꌖDz|t𭐼JK͕8kv-R u95Cg%+c55M}4g}"ȇmfkp?峽TYmn-˒GoԚ;ܛ9{6 R0C!dm6whte d+K!B!B!!AB\1a{R՚ŵNA~s}e%MXYm:mShvf?e0"tLVCa(#d%MVAolZkyIzI 8z}L׷ i^5OvpLidpLnc( +/k C;sZt<Ϻ{+es^r.Q#/EoyA,6EUx4ínvVplMYs?y|u !B!B!x=$B+V3 f-8o\Q#?? `&ޙf&o9 CaS)mOqV[&y9,qZ3<)*Im) zce%[gAzx &-cyוW5Qy~L`+Bxo _3p:.e3Iuj^'F?[Io810K`O4S]2.e];ܛg]{aj=k)m|ĝwY n}΁fſNQB!B!B i [ӔZkV3JxO/mmqvo3Z)e}90-Hˊk:ȟLQ YAT\^#hFO lcuv8Kvf^W~/hOGLh\jt 6K yO_x]U݌VgA#ߡ5Qe[4,BK푗1^qMPx:pEĝ\;zoX QTB!B!B  dXA׷il,C1Ky~8f%%5O6iQ^QähF;9!RЛOi^^gU&9gY3Z7YWV7蘴֌ y6ZEe,^`c<ʘ.>Z$"nVȫZe.iqMLt@˹wۘP";,Untpo :i ˄kwX oXޱg[0L&Yi>B_B!B!B!"AB\JFY`N!Vj؝5M-ꚮY&s&8>㌼z! -HY#T0$'-|}L2zCCFD_WTT0X3e(E۱1ktP|(t:,_,OfJa(Vsκ'؝>P YU$?C]!/S gf-tdR ,2XF[ݿo^͔,ߋߛ=oڦb5lӌּC:2JxVȲٝ>@_a!B!B!I/W>E-{NXx1 BZk&)՜MZWhw&)eY 9ZE&0,} ӂi9GZo7M8PU҂iV:&]~hXӼi)ӬdapCϦZ2\_Xm2e ?紨8s,Cўbb_ GajpF~7YYv-ǚ;Vz68')|89JY#ߢo(Œwʯd]W7=d5|?t67Y -|B-M^%L^Ir7h^ד]Ǵonde|˰ 61*8GX ],ZF<ckJYkvmӡ쀸0N/.rF뚢1?1;sYk-dkrZB!B!Bq$B+Ԅ)ZkCwCٴIJ5K'ـ_9_=dwքug7 J)Efڄ rL~3Z?KsFt= 6M&Mhsm݌7YrW}}n:KU(x`82թ + ?ozWAϳɪQTѓ 5Q`kGO7MFE5ˋ(7)낝ÅYN3U(vCmۦK_2.q>B!B!Bq$B+5de5 J`G6J,$r;;M؍S/7oi$2b-l>LgU]WK~5KE,W|4%5 -+BۤZ l$K& +yo '("Su}nM{5qQ1NKQe~[[,y6U$xVH[s8G }YT9Gx+ 2)Ѽg)뜝^4/l3q1?2fj:b(n}V{cJf!B!B!XL|!"e]3Lgckͽ-ǦXe?y {+?N &Uk}`lc-<`kWQZu rqc"YlB{qX8mעҚQf? Mev3G6m27:Z?)oE ʼM, 6fTZq-Bw=Vlަ儴fk\T NQ/ȯً2RsS>%-#nt[k&MF<;d5NYlO"I~C G{ӵͪ.&|9+ 73fß>)mM;v?d$*lMzuճF2qNz> t=x5׏ Zk 6߇IAW]%oqFgDyFql^pQӘc-ZbA~W 4X Oկ/` >},4uȯr q3Zh׺h9M_uBgRk}ڋk2CVTd/zD/~Ca{3.|D^Fl= lg+J B!B!JI/W$jf|f{=3TMoBroF-: |K5ӌZkA~s٬^銻?O5S>~5 }h}ՠ?L qQrs,yM#?**oa߷M0FDZ"< }?lջsc'NkwxoS| XV)uI?ަj~nx}p)tۼClSh ӌjo[.K*2D7g>i?RXFvߓ _!B!B!BqEf7PfX0y|K}ko7nJp(=pYj~CA6Z?0 Q>~ZӡiV%RB V2 ?A~%mǢ\ 0>~C Yq|gcd}):l羇mv ӰHˈ$(r](sk雬1T=rPԚ 4f쿽DQg sfU3휼⋃t%[=<6 Bh,j+@ k Otӿy=bkWqVP6pX ob?~JV>` B!B! _!H>ތw9\ @ֿ?xM6!Ɂݦo`~zk³['(؜5[|rK L2;xY7c/b-<A4ۮE;b8+g%;42GfoWF94c[N{>>+kǂ|5_ARL={&ˁMQ\X>=.ٍNeE?.0TӲWBi>~ ntcα 8ZxZlMQ]8lMwAaǗf}7IAV/ \+hٛ>/ !B!B!x$B+RT5Q\ky'> O ?ano2 B|h¸jxd|Ui9}V IDATqTK)q8p|Sz߿I1G/CQFY|:>jZ6IA7A"tLbCdz4Zq##ڮò]p>4cn^ ȏQT3}@?Ʊ<6or[&+CYk.pL`ZWM;"jIi([.wŏ~6롋F3ɨ ]ÛԺb{r|}|uM?GvlujfAڋy,b}b/zD^I/B!B!WE|!elgt}nZ<~鿥2~>/=Sc;2$ǣ_h]l$/qM%2.%MgO_ZkͿ[ow5=oXyڮM`UD-w.vȠڄETTOˊi^RiM4EI/y-0KCa_ ݅ͫiVW1i>h wr]o S=[m*:e3_1}M*]5s58ǜI֚O&{\Í{-eٸ$oN,j;Zؚ|E]׺hoNOwI)kMnuBr1 |Ӱ~ vUzB!B!B !ȫ&,X SЗu݃s,n~ʭY ~R 2m2KMGUYl\@]`>e(C)<`xw|gs;b7zwϳeu݌ֿ`#2]6Y ҂ca^C;j7]`ɵϜph#uڮE65p~}h(g%΂y/~FV6mw -׌ߝf qMI]6_?l쀽XoubRX+MUk,åo`*aG\LYW7{s?~ʃ'xV;dzBC־bwBB!B!Bqe$B+5qe(6'wlMW[_q[_wYoFl{b?Ȟ =+`9|>d*k4C-mC]2 * 6Z仛mlӡmֺIRN7幁<iϷ˳i` _@qNZ,y6 :Mh[DE({ is@˱X{eNϷ+^`$+%m4k]ӏؙ>6]w÷ۧXC_.tzk *[gUM?)F~=.O[c%k'g(iFQ26KEqoQՋKM7*ew}_?(X ]LCOi+-,9v]G!B!B!l !ʊin<'!^ݣ0Ppa(؋r9CkM* QQT9yMelv[XF36=tx*R]`qgA~׳/z]VksGnδelByA~U3mFۛx'/ufkMYZ`QZ2%U:gK+_!B!B!Bq α T# x:xw|wk&+4M5:K\kCQg<*jXG?#\&=Ήuv[K@)ţᯈх[f4 E˱N]Gr`ςIVR%d~̏} ?fAϢ}=yIh4Q^hK&t/ F !)&mGuF~y 6,bBZ}8b:kEOwWOnFf{M:^&ʪ&+Ҳ· ڮbUf]os-C9kFF~x[;:_L7MkN :x}\ 6ʚ(/)qa?!+B!B!BfH/W -+S)6[nÏ9z\kC`wN?ȯuM?o=B*xcz7X71^x]Zkhq:E}2%$f7zOk~|ٚ|Ň?>#ܽ/ ZE˱&xZUgUJ iٌw-vqxv l4wGCfh`i;=J]2Hw|d&ȯu$p+Lw约jRidC\Gk]e0-M]Bt|qOOh4Ma ؙ<eى͸5Ajx>^JXb$ +`6{'@oQ^rop?G'U]R늻?gSk0)(lThm"-jIAQe<~?k$EE˱6I9-v{I׷) Ω.01=r3 Z3JiZhۆj*Iˈ[K`[\S.jv F7kXoa()YyM܏r N'lO:KOz6 FIAZ= `6Ri !B!B!ĕ _!.YVU҂J?#A=z]1iUyu2SJmwޣ-CeU3Lr [Yp 4G7:a$uyV+?@wNV&jTk } ߳LZRGr{|{q.Y?V]bL#w|oR?{o/E9yUmBY^Z| 3G/rEMdzmU(ݧ2 w)f{{1=U*]2H>fL1Z{P+k 6 [ٞf͖a_GُRqCR 0X _2m3FC _Z2ًw7 O ۓUF6}6 iA\.@8:2QGZx}!B!B!l !%KAckǂLouZYoyXA?[t'GW5qm hn/}?·۬e0J Z: ŗI.8c|¯;Hug6xf %;쫺9Hq^mw.`E׷Iʈ(MLcP.4=o(nv)u(~OG뛊5%yq-Zno0l=LCgd ?\fm e=![7 ~^[\Ә] ևfZ6O !B!B!x$BK51kgp?|0١.s}u6ZNȏ|pY6i]0?/}}AÇ[tU: 2X Kʬ;1iIϳ1Th6V44ؘ5w&0  [ ?ַMС%xs1b6a 6`(1y9ɳɉ1bq7>8VffΟp[ŋ$B!B!Bj$BK$~n'Fy-V lO)gXzZW<~JQeg^^E9Ѵc_l!;sn-}Ž8xFLesk雴.t 0k%kaUǣ_c>@ssqς_ 2AuymOЏ]n~o?"G/qA\Tt\ /zBVtuca; Z=I9o)`.I1%'3Jg|d3d?2l[g6O)-yU3+&M[?5}ų…x?XP&w0 Y33>ܛR\<ۛF?v~;ZE`U4/=dѺczmBB!B!Bq$BK%18s x]ڹ:X=*6\nwYYw?3ȲS瞜gvw6b#@(H"@J(TX*JV -*dˑ4H,bs^lىo|_ܞ陎=S5;}=٪yU;$Lm^ast!y ۼ@RGj n]kXwt|l?V_g9a̋|Ὑ)U%q5ZAbr]i3TŞ1sm9Mփ9ޜ>g/c=lEaKIBwI ? j}H/l1g M.-IOlanC]_qբnئI9㐦! {R lc0 ݋ ä ª._,_ikԻӌ2V܇e]e3e#:*³2{3-OFAM.uQ?,y6ik4Ys;)ƳsVaX+G1 EU(Iip/XTΪS35 3s, ]0Ii*]Ɇarx۴5g,u̹kv㧤oN>͏!ϾXq?=[ Y <;=?c;"M0GXA)ٛgޝ3E$NL:1(s\h|@w{w(x6Y{QН&JBʙArNq6ꇿe#AaX.) s݉c/ו)&N9SW;aP:dlfb 3 . :1 ?³MjY 912))jl؍`,Of1 pZm 1 $ҵ9ߝB}4bg e<ˤ"+2 _UX EDnNXI%X&aӄϡ)e1k9_X&լek{m6qn|whՎߘna@Yen⃹8=cʙA;9RGkhaz'9L< U;4e@&I~0>}9 /xlϗ{*iRl2I;Vv6asEU\z-_k;:oN7'HgX%:enLܫu&6unH/ɻZm’EN .frx%)3d1]*!4a3#ۤ9/ c84ps2yi} iSG]+K'YxI9c`ygg7UcԲaP_z62R؃cw&^ڬe|0ȍ3RK)l8lIQu@nR _DDDDDDDDDD|D{!K{ .cz'7>ђg,B:xV]#IzQa\76WIIxs%kpt!5c#uU2 v k`Y#,Գ@uikt wsԻ EDDDDDDDDDn6""7QˏwCK:? v:誏W=N3߽6/d09.]S-\qWR IDATqwf80p/r&Sw4,~jv38tN'T Y$` ;Jn w'iD[Xn#Zzi[b']e3,uoT#C]XBo iA ئˡp̭4nyqk%W\RRޘ|^a_6GØ^8Y;O7G+؊@DDDDDDDDDDn,""7Qo~G~9l!c98p/vjrz/Ha9D;\d;Eލb~72l x88p/G>iG-s#N?E5;JLWNҔ^PXA5ȏs wpg(x59]Nڑo&w?JѫqzL]JSm8I`~v.3滓=_a|i|Ʊ(zIWe0YήXsm?d OIi#jQ2k^m:*M?-w/tC:AgD,In=;=jL䛆L{s`0߉ew~G~xi?.*MUl2E/JhQ{Y9Eا/_1@DDDDDDDDDDn,""7I?6]ޞ~0cNX} \\dV,g&*Gq Stu0fb[׎·lf8QˎmXW%u,Z~D5 c Sw'V&)s]o#`+p-N̾DB̞1 =n]EϦtÄzw~rfʙ f:kG'i|g{НfOUJ7mR8 9} 9[3o4e5&4ZAt)yxֵ]`08h.\!i4"MSin[#Àvman7͕[AD+l hs6qd,sI wC|g'ޝ6ٻș=,pF o36ۤFWsc Fo$""""""""""}䋈$0GXAѵ:ϾiX=- aC:s{wZ&i˜^~匳bN̾L'lrlaUb6)y6e bǽrG?of2ȏvARSA/s;o-gݺˤxI'}2v)5-D5$il' SMޱk_4ML Xq2 ՌC p~sG;XwcSpmlk4e!Q1[#6 kQ8q\7$ڠ+2lّz&E?&KEfڧ)yn0 XT2I U&\{,âޛu4Dmm,c|`yVd 7Օ?߉kgw^S/_!dž`U24kR\B?OßcS/"r4fq,<ùQK18af-됱Mײr l~H˿u?q,˥>Ic^8'v.z[\]ϻvӗ wS]fh ˷qL;0`a]ݙ85:Eʡ-T]עK#˯0 ~Zn(Yx&̧)Kf;gwȻe>7Q:cq,*~XZ_bgWY\5MIEm^<$Ac(cݮx4knt[Mm)ȟN%^SLdzorf]k=#4Ɓy)Rp+iL_X߿v+2[EJvblj s򴌵P~a7<1ԻeR̴snppM_WXlܑPˍsJ4zs4z EDDDDDDDDDn""7IÏh"re5ylGuur.YǢᇴُ"Hzw'F-AL&CKamJ{/sjM,G+BY^ڟԻkƋȹ%g\X-Lf;>00:ˉٗXNzGDkS8Q*9p~'c̴έ=IS&>I2TpWz_Nŧxwy3~}ջp uQgmhFž݌3:7 5M|¥qOIiuw_qjM2NCQrkkdXH1L7 Vw.iR= E1Rs]aeKAl;n4D-7m:,fEmZADӏHq:c[sJf =\ۤFrxVfoHƴkE3AenjQls ,m "|_\k[ϳϑnj䋈$^HÏp:9% ?r]]Բ.b#b.}tr{'BCC8DO\st'jYs7&@&<[kݎ|4)djm#h)$aB' T(␫H1ԑ`%qe7K/lGA@'8K)pvu0lzwsA~S0 ?O7lnkwf஑nz?Z\^[VH&u=l|_mӐ;Ǥ 7 v L.w p-G5 gRj9Жkw_HaL< @MX+DIȡ[|^0Ft"'ioKҔFߑwWtw'^Չҋ:9SX58&K,0pU>x}8YO|8u:W/0X=99^u$a0R00j7ё_"ȏ~G~7 )xZv¾uϳ!csa.uo@nfmZ~DˏExWcgG ,}.z!M7 Zvvt$1vW☛@qJ!'e7t(eyZA}{CDDDDDDDDDDn,""7A.AD7a9ѡ0kǟoE-뒵 9vtgsFo8i/u他I5k1ߝsJ58Vfu=ߋ{SX-1߅gL0۾@tØV&y}{̴/|{)eTW({(aYW1ScG yT [-NR&[ 0wy~|c^xjQoXk26aECv'![AL' J:˟v{3/҅?kqچ]~p<;MӢUqLvРv8dzs;]wEG؛4i~cŵBF {(ՏT`ųLFof }-7Fɳ؛,,_a`&GV{G޵;Q b KJfWԩwqEDDDDDDDDDdk䋈A, yZ9*!Ws=k Z7deG(e i~G Yij}~< uՕw- M4?^?d+c;DaPǁ789:pÌocQ ?Z3503ߝÅ>I c{iR:8I\h䕋B$ʇy`g7]idmg/~:v2\|wwg^$]:4zaP8xK#as҅o؛}|jW)e7A-& Ѵce(z5Ի=$2; -ȋXYc {Gu>VL{vab[>EȞOɀDcGk?D`>#kh,ti1(&Nts}t.4(x6m cZ_ Qp 7sݏ-"""""""""" _D&h! 6#\k,*θlt)#\E~c=µ<E[-/2p^߽pG {ȍ-0e`vPsNC#c\i]gGc /$M.6O1>O̴@RX2>ϋ o1}>mQ˹It']{E-ޟ}vHL}8:aG]>?t,wľ@nl%k \;܆a24Lgi>6};)k2e,`]c nS$}[u2G>r]iPڸIÏhmb~-;F)j,#Q^v}%&telUhԻ GDDDDDDDDDDQ/"r,"AA߶W29Ǧ46ƊȻ+mbM^xvPgtF>N~JCsF *y]滓\lcbP˚6(yuוs,Y NY'ȷM~KXNsb%8a0R`ssR3w?n2ȺP9Y:L93|wf_$Ma)y63y[|]|rW-R]+f0~a.-0h6E26 wPɎl =ˤ l"7 jv0nu0Mղ.YǢ4*zn( n/0`cu2N#o f</n[%{,q# """"""""""S/"r{ "I"({*ǶK޵hw؎A|gs {!Ӽ12NOMvl;߻a|wHa7C],f`]7Gg ?G˞w-Y Ni;VϮQZߠ巘j^ = ٳĻc`gw̭Zf#~|#C >}$b#NRr.epa};ǜ]|b/rtu ߀m#?;i̴(2ܖ@ႇkwMeףFZ.@`(˰. |DžƏ0 OTC}-9עu &`0\q^mޘ| N=Iß᝟Ow+]g ]hu5 jncx"qb<{9x~ WӀb0m[oqbd =\d ׯK ^P@nl-u϶Z|f;%:!0rsI)cSԑ U)xgqzaS}[$e EDn{1LaBr8.?3^ p奎K\) rfѠgu))w~܎m<^ 62m 0?bL"`G?ɮue~ s:uU2ͩ2ݚf 8>w}Zv#eP8{!AZmC14a$[LE5;v|ÃZu3 ႇe|7Ťiz0(g 0.&; X8`0XSN[%U J2ĩ@x92?)G~a#)EF %vn[]y&XqB+\s;Z[''ybo$5) ޛWDDDDDDDDDDDF니?oa~Ѐ4 Kj~nyEѵq,v`p~7oq!IC<+b{F?en/2)6\ZdqWr-;h(q#I"lE%S_"er4)869צ%,tJv,.vӋ](2Z4 ;>haGM c3K !qvPj[.ÅC;=E+qt!rGnߒqHҔ^H8K F豻Gjۤ9AÏn0-  `̎˲(y6i "a}^3iHf8ke.4(x6{i:?W/.еHR0IRN`sr.﾿mue>Qw0>a@)wl>/Sͺk&}$I|F1w-Jb/ f(È:>4Ž۶ߕ_:Ի!/_`-ݲ^D)9hq'";ˇp>b%i2swC^Xg- &(#MM,sR;50yL|ޝmau Yxq?Lm깲ZųLN/txBgMm d+:Ve9s,.4z|N9b8 mLg @Pp2;/_3[dB`k# ޚ """"""""""(1yA,y6Ef{g]$cGyCnK M%pz?%"h/ 9{Im2ssݍt#`"kwٹm˶ .'f[<n&:oK 8)&Y}஑({8z (Ґv@ȮC7ksX F_)IA,?~2GƶkD?))~9N5[06 xI@`Ջos'm%{y,Ӥ~<*{+#ܘ,,3b/}ag?E,_*k'oLdN՟uJsd|9-|giAvmȢgs\f1I3*XWL ،vaWd}{_ɳMvWdt$DcdJۦx)xiSalW̻ M?}([{uf7J"Mac(c>g%Ϧ4\}(@F&nL]y`$/oS42NʾǗ;׽!5,S/"r#=O?ٷ Á/] jouHaϭ.cMyL-2a ?*Ǻ+/g9<9tKF)q`Yma8IR.Ts nkmm$|{#;wV)"""""""""SGAȍ2 *Dn)86m0 V4n.aPW;nu%""""""""""?;䋈 UnxZ[@DDDDDDDDDD3ou"""""""""""""""r|ۈ|ۈ|ۈ|ۈ|ۈ|ۈ|ۈ|ۈ|ۈ} iӷZv EDngл'O'7ݭ9[E߳r+h񒈈&((((((ط 3s;jU*mVnG(󘦱|+t<˲ns I岫vxi$ 1ba$5? ls=grj^qB$b l~p~E!a]sܥ\c6}Ab86?۟|^y}0 ) /,s|_'w,m1K4e3z=_^q'_>pܮ&_uǑ>s'7p$?ל~헹c|{O G{E G^}W߽Gd#~'_"cN7=w?g~aqyq@/| =~'O[q_-zVr;w~|]sK_wvWWVE1_9v ߠg""""""""rR/""7ԏ9?/|G?J+(gѤ>8PGϽi|ȭoO8s`rzjO/}G y慗~ʲ'V~fyGرx慗_X} V ""?ݾ?}1En&xr0:<8ruxG(s:}f*?Eh`\= iArLJaw,Gfᕈ ""rô;7=3KXAUOvCP3O|~?S?LR&:iy$w7w=蹗h4| 0 LrK=B>Ǿ=6}VMۥT\~7;w;y05= NYP@DDDDDDDgF니 3@TX"O9z>ÅSQjw:rY>v<(s0WOѾ*z簖&FK%۳\yg_ؑ dtN닔7ȿ):.V7z9ȇg߄*V|{;?v ,OG{O?_/=>\VEDDDDDDg|au^̽ `bj\&=} y?n?'IBC|GSxf2dZ5̵d3KR_^om"2ɭT-Xl6OR7ak76<׿UoK[DQm[-,&rn<2O=M'.ӝ ϓ)=<ο{v{߸>˭{8vNg~yk~_-8i:vxs,?-/ r=ɗLs] c,˺qOJDDDDDDD6VF~^ϼ@X\lpqbjSΣaȓ'f|ld.}`('>'{hyB:tѷO!iG3O<|?osLvsG|p,I /LZ|=UvI$^brzw @~\wc+c6QI?G8omELjoɷomExAmxA̿Ɠ|H1M}kA\>V:5?+~c??~)H,WUr5Yn}| cUܿ} ?o]__ftxf/?aEy[n~ ?0 7é+8$XƤ)1~ S}Vk]ReUnbcF*91^I }ncN^ d2n67$I>18ꕕ[*$i4*e䩦NAAA$ƌS}M_QeUFjq }5r\ UXXh_ 3NvA>[E%zwdz-OIJ E\E_lݥ_~Iz9":3;yk›VdD:;I3Mַ|}ap\n]Q_=0 =r^w);O7P~aqu&[n{^ 뼘DMϲH=ک|~aO0;~>uχƷ5ZХBdd24iX$'JJKo^;ڽ+9EEEj9}"##$Ig]~$sc'*88H+ަ/R``9CGw*RΝ$٤`p\.v'i$ۭ=UU/,lRRbΚ3*<,L.[Ѻ[|٦_&ƎNcVZJdwYeƭf{>Kq D?:W_\.]I6"#7?|V#[G_ՋJmٱG>ʧϝj=tݚ3sL{vYNT~a:sfT箽%IO>O(IJNJח{zsZ5`mm*u 3uT$i۴;dv^}{_M4kEG6ÂyU[W[WT#b_@rѓZrB%Əj}GOQcSR=#IZ`o_PeUNgdj'OSn~j SNxxpcD>BUH}` ~I8~,^(I:vl\pռ3CW#-SSt=j 3S7l6KB|I }+y+%IƍZ>aIRYzIr8ڰy$i***vc~uχKp1IRhHOiy%>w'`'"<,:johHBB=##`햵&Ij]PhhmબхI97}H}hqr_>; X,==txU]Sё\ N4\]/$QQB|B%IyE׼f,}32Z&!ժ*R9N)2"\}~565)>.V,]Ş;hu o}{~NINiis 淾m[Ԥm_${ D?:@\q t nBu0<{ I)=AմY9*h r [к-ڼml&kV N4\nl-jiKjҶ;iںc$iikV-^|]թ3r\X2MoܼCSƏմ)x)00@f2!e_߆;TVVjUTVKm]:SFE^U¨8U>/|JOMViY&V/r+nLN3P\! /\\+k/Ңz>=-E?JKIjŦ1)Q/ԉ2 CkV/llSaq۞ Bz .>y!K/yB@nU=<,^0UT*22B\?r$Ϸvk;:MvU^YjWF9١/u5b2]CΘ8m]][SHHzFo~  n6I›()1^M]Ig2{̚>E>Ja7_{r\ݖ v쯾]Sm]f^TqIأg>Ȉ К˵frl- ˼e6=#Oǟ~&UGGfL@χTndxb4ڶ.:ҷг長UU] }[O(1g* {:*HAAgWZ3`)e6ul^{k7O{͗GEz'Ӣsu%叟ӓ?(0TRVO7m}%pr[;n]ؘ:)[o,V%'%()!^M^c]>q~>oA>K``ٸVrz[j<7 vC7$ubEFDlE**2Bo>|e^6oC#){7S[;A}JfO-7ϓ$;y+lRaaZ~mMwJVu~?yN+ض7١_?k<Q:iNKťe{1r>QyEzkkov;z;nYTPX,I0n FKchEE^O2#DW&I*-s=6m޶[t;,ө2IҼ3d м3$IE%r05١_?k<Q:4a$)7PNؽb#:1[GZo ھk$ޕw)00@T]['IJKM*m=ָg'gwY&+uyB|\qm_UŢ8-Z0WT[ ۭ`Ŏ@9.7 D?6\ I4vC~$I1u:-ksmm۹Oh\͞9-I|.ZZ4:-EsgM,oWV^UuuLюk&K9!k}IY.fHO;]$ɤQ^k{aIҚU=VTdQu 5uji0 EFi>]k?Z׿kKC A>8&I]ʑО_e6=׿yA=\f^$-iꋍwtjq2igy``FHs>qk?b._V,Rgoϯ}KCMP7x長T]S"ef$M8N+󪮩$;xXKo_Y]I|}?Ú=#o*[;kFz=cܣ*7/h:v?J3O00&wݡw:y&C1#UQY-I 0'QhHv=uȈpEFFQ M2I !_s2 CkcŲ:1[.[fz~\nUTV+#n?|5^5hRh=R IDATTWߠfMfYfO׳O=+%>q轩&xfd^Ŭ\ue֛&}G}g|3=4nך $i(=5uNt\.R$IjhlRqI' o~l> ۭJnY0W]L? ݳ|L&C'Ϝ鳙2LZNqcK?2i$)00@ǎ:}A?^rq7"v\0o}Ǘ+~e?txa2ӭV?t4 ;4kr/B|VL;(,Vkr dv?KŢZ&b }tT\R(91Af?Rΰ{n[e*RZJbFFXtT^QʪjZؑ FȐ4,ۜy)GǏ=~WPXv?u loz,0UYINIz9:qtXgW/?;,s_Rruxr_%gj}Uhڔ>  SxXfRFx3 CIJJ|Ol6>~pm?u'7xɿx0\F#|A>~ ?B!Gn :qp _ 9@+·8!0nn[Q@ F4Y_!--*[KP7 Ess,`g!~>RjdtP]C#F Kn[u *)A9`(γ u`81 C1#)vdJ+YdupHN:IKV̈A 8p`,0Ƥ&iLj$iiz~.@s,_γ`@?kK/dM&f\78< x@?jjW2α>|GW @iGrp,@`8sP9@!G#|A>~ ?B!G0.\0M|F`;vP7SG#|A>~ ?B!G#|A>~ ?B!G#|A>~ ?B!G#|A>~ ?B!avu CL1Js}6[e2^x'{Zat5 A> }a_V``"nn[@w/];5>ݴMuar8|$.K*kӬSe2jikˎ=s\n^;]Ț{d0H:r=j͙5ͳ<((PݳL ϙxn8 565I4otEFFt瞃 0K͞8!Y.)bס'TXTА`M0VӦLw:v+$ӥQq17{bFFwEry1[UbdNwLCQ;Ȉpwdr:1[tutk_Ш|E:mW|ebi$:G{444y][wUKK{ [XIv]!!빑}Lj|$eJJ%{dRYy;Kj _xm}sGQPPܳO=v_l߭ʪVB|$iνھkOIz.7˳ߚ:_ރG+UU]R]Zh~ň|.KUaݖ PPPgDFFh̩W~aQvulRϾWmoЄqؘ깾0YSQeUw_ZX ISiy,]j{}b]1 IRHpPַr=n:~MmréF0[$ C}L2QWJۺc:|$߲@vC77۴eEFki=s-,OL$tMk).6Fr_ ݮJ g=%٤{] uF1ftn]8OSCCn;K#FDB;PuM|{mwlb3BEھk&O ?%)A}>v566)*2Bm;Zvv>\nY0Wk>bBx|&Ug]PJR[ ӥȈk8B`PY\)|.:Diá? %04}D=x-}{P'N$.'іUڸe8S?Z}vIRdDΚ;n]sqg*at|Ϳ;L]7]sW/s9˯|nZѧ`عW㒇]jl^q#ioZxj5":R]g;TWߠȈpoۭZ+< 0EhT\UŎ>0|.٤k^ F 0W2A~, ~ ?B!G#|A>~ ?B!G#|A>~ ?B!G#|A>~ ?0 3M|F`؛4iP7SG#|A>~ ?B!G#|A>~ ?B!G#|A>~ p*,* /~p_J83͸!446/Bϲʪj\!lUgg]Ћ}kYv;I/样VNk{-n|c9vjH1Ǯ\/k +;`2l=m"'`7Ϸ~)u TzZʀF_=oYQֻ\nmܲCcS5sdu6[8b546)!>NOݩWVN\^}uvJ?WXXO0 &'k[̋7&uuu*.-?-PJr"Tfٺ֛%IE?\E [[U(vWV)7ߥ\長ho_TVUAƍZ^RZiC28vg]PAQbFLvn":]{[WsqxtMԇmآ̾CTX\iyUu.\ʑl|pIZ0oV~_2e&Msg)0{9]SJRn?G&:$\GVpPΟ>cmn֥<i}jgp:=ˢ##4;1[n1L3sBB{UZ.mkƤ))!mI ^ Q56Y|%>fc'垃ʼ嵼D_/jC ָ骪ow֮bά{>ƻu8}ρpn_(-_^{WUյVgB(ݵVM0 T;{#**R\"׷N/)00@CЪVp첲4~L R| 7Z\SRb|Q? u3:4wVeNv;ew|Ǯݟ_ח{s]nŶ]zd6_ҞlڲSNVFEfCdVEi_Kǿ)é 7dוUTWm۹W;waS{+ѧĩ N>ٰY\t{]S>#`=н^K?E jڔ^ZZr: .)ժPfrۭƦ&Ew Uھx=wr 4vS$Y,V|^!!HϺCGNO6*=-E-rҤȈnksBz}fXzéfMa=b*(0Pޗô$$u*okipnbmVdDDcqؾRHppe|S_TCc6fuC--n햵Yv[/Ǩv,~]Z[cE&P߶=8wAAAAqZNl--]ΒperݪoPxXXwMM*+Ԋw*1i!_jU9ZGvr uʥjnd2u^ɓlҘwܟjUpPpS!ө`eiwE5=vt;v.qgy-_V-[rvگ.5s\Cm>~Y/VSI6db*2wvmݹG<&Mѯ-VŎU];vS}wKu -[rU0*N_V Jaaawyr߿Zf)\;|B6^{C%'&WzkM:1Kn'{@Iӓfd^Ժ[<# ؽ_;PsM!EkbOؤH%iˎ=WGOjm֜SxyU^YW/I o3]Zp~a֝{k! ›HNк[vA@YӧLcVhty߸E52 ŏ3n( ͚>EkV/ 4rv&UTVd2iʤzՊl #<-w_?ޠ2boKoLn:x$ithQqz'6zD/~j۸Esp8GI{}\n%%u wIRtd }վt&>\I/t* @?JMIRCc֮\_Ҩ=*M7Ƴ /*nF(?|XO|9Fݵݟ_y[Fo]dTV^)IJu*WWߠ7W|_K;vv9)͞'˿ѹ4w4sWl--ڲ}=)k#FZ0o~tZ~bIk|Y9jnnabmgn;k^ݡoӽ+iMsdZg[tRd~=aYEoܪKٹr:]  n ?x֮(C+"Wx_?{YLN5.c~V$kʌx|IRtTnl-rݝy[ܫ&)8(HU 8:.w%C7Ȉp[{RDx~aIR@[?rn^~}k5cdU*!>Nn[tWJPYEVS3MVŢw?G2M>Y߷.%Ʒ$r"Rtz-&_HI :.6[m(b'm7sOMl-=kgGzݏt5wֲo IDAT4sWv^xM5lZvǭ6ejk5vt+TU]㙞Jf ܬ=֑WP&E;+(RŪm7c|&Y,V=ܓ QiY'/(,^zSǦ=+2"\Yy=:a2Y?;4ϷؕWP{/Semn֨e)44DO>#UPX?L9HsTب}iM:r9>ޠ?zڥ\;Z`n9N}uVcһ<5pIh 3/dDxlϙZߘ4ӯ~ԓn%zTRVT/ǨvKMN+;ws|b$tww_[Qq,VR;͜WzdBϨR_?WW~CψQZ`9_8|:B=// SB|**ul{pLqr:z5:-YGJj}҄>ߗr;1>0ʳZGۏ;3zb(mܼCY%+Ԥ{{u>w:pn|o4Wdz]\G #P=˗ZN~?Z=4rD֩M&fPj҄SIjcT\oڦ-:FfOWum͚m_S[?c-mQDo9.G ]=j?sbjl-- 1v.T}-&f}`fN =qFA)I 4aw()!^ q*(,} _cUS'i4M7ZkoܪX}TA> }f-Z04%m#oYfw!5uv5Z4exS&ә󪪮ըY͞iq^y[L_=TTTBC! SyEUZ6z5uLzLjjplH8y_Z^1)Nޝ&k݆-**)T\Z1i^ϢHRQۺJ Ҹ1i|Fi'髢2IO諾|}oI 1mYN'OgE5y¸ҟ_}GǦ{=/f:zR-kZv󔞚 {|kƤ{Y4~lHƌG~"-ǵΞWm]yN665R+i]VvƦvz>y_|W9N꾕Gx56Yn0M}Lww0 TcSZ=CHVNN~jᠼJi)ʻTv^g͘ukzŋnE7]{ii܇Z w?.[}j|)%S}!bmVR¨nZDD$YRȶ[pPNn?[̋^A~ YR 3֝{9r{ϣk3{c=zr;!~)iBe"m;+(t >Zn;NZ-1>inZn\.`:nWPTTVu/6&Ew{=S=&%r5@6Z=Zj/!.IAa׺rulvw-_꨻r}{:p!G{ovs r ny&GgvyP6{0g|3瀾}>@K⺺zw /ۨnwxLSr@[o> e ]QP\JxX(a7J.aX\W|nAAߟs "Wsaq{zJl.s1-厗.:9'MBC18uAaQ bC3@,_CaQ N em:˚ [).Yi都U^w>]1xP.e9!=ojnpt:65yoʵdr۰X_>w7jL:]RILgǞ;Cs XGKsbIGkAl6;u>n^]}=o-k6aNl477P Z'6}RX\ʈv47pl{*"8C{ZZjjXqQ$'u>݊L=EDEk7d"]ZdÇ !6&R__O46Z4IF#sfNe=ZcGrX&d3gTLNSS3vC.=Jaiͭ=-DFͼ93Yi;6J~a1ml6sm7wZ.??3Md|n3%44KH\͹i*GObGs Ju0N0d jYr-Qd Һ)=q?Y3d$Ǒ]IL Jjz %-%[[Dd:sXB T;Xn6`q )\!7t-7|bdW{m$&QW@hH0Ϛ [lŰq.;sfNswKuWΨ8J\L4 |)g>Y93J@TdAdv绣ήHJ.].,ֶ9KIN>;wv3Uյ|d?sD3St,u6AATTVq]޶6w0"9y={m]=%HNLpu6Pw۴du>F餹FrR X(*.ߏfi)IX,-Iy_u6}W446RV^AeU De yK\l yXƸ ɬ| fMLppY$%wۃ,`u|+ #rk!!#,b5{LFb3<5{1nkqb }ރ<ݖ{}L0_b-{q/7[{l`GtrX&;=<;Q&~NGHIJ 0L@@q1 fϘLn^3eIA$%Sviөd?n1؊Kjf\ZFqXvߦF _|jBohd :Wy\.L~~uADDDDDD_ȷ[fMcsxGYjV-siH17wtl6;`}LFfNȼ9Wn757{ ·ol7o93d͆mbo;؃w3n{A>Y}a!<t詎z*=wʲk9q wWʴdyjGOd-sw?$lYضk qS_?uv͞1w?=R@|\ 1Chx={o鲯 O3 f6O~Os̜yS&`0p)V$x^7MDaq)ofպ̟7ƌL~ƻ@waZJ1ёj2z62W횿뮳v;y.X_446RYYMdd<1 wco䵳kW4W[@"7==;r<ϾXElt4ndTF:iC~7?ٸuw_~c>\™s3LdႹװD""""""""Ww!EDOƍ8z ߔ`u1DDjk;j_ӳO<\#_DGߖB|M")""""""""""""""rQ _DDDDDDDDDDDDDD:@uD|ߵ.wIvv6C JSҕt+]W?z,ҕ]N\.W C.?,;aj_evmy[,_T"7';_bUb0~ ]{tg'<\^ڥ:K/xoSXT-7;,~GCc㷺_"=mWk7}K%w "]v6-NM;~i)+*ضk?3N$>.kb- ȌtuMM{bHe K+U5p^ݸ_ SPT` 59j8uu,IGc2 K盲 .ȱ JHp_60inR*sr1L]n|n>scQDGEz_HQ%4'2OsB)I L?m\*ѓ<~4}ڎj%ByE,7O>pE2vg]ʽl232@sinq2 E8.n;7hH+.)aCn_RZƞ'Zqǔc=E%,h)6a郉!w>cF}}3j8f >,cd2ߝ2**yF$ߖF g]{Eںz^bg?E|*j0vTy;vH߲}w 00ؘN6mg̘:~6fԉy|^ƭ;پk?6A5&OY,_F~~&. ]oYgYzUյb=s<4Wmu;fseႹ] k\^hdxw;Ć;/ g_؉,m6B;g7M]QOX_T\|>FZjK༰7 {qRS\^ȹpA?U{Gʪj>]y8.qztt}͝_㵴rF>2U1#x=_.].ra4IMI;ouׁ/V^KeOE 6d0f\._|Yg`02jİ_БyDѿߕ5,_9l$MZJsgO%+8s%b3yVpq2w;]NIiy<=]q\l۹y7S̛`=*t (:zQ#z_s[ 4h鱞LlfϘܫme>ӏ^1㙧{l8ב/fQ]SGTdx].t9@OyȤr9/="""""""W""kXN?s0`|1 \)hX0lٱw0yƏclز-- n&dνL<.]>.gQq)fsFwG#2ɥk[;}~Zt'ql۵%_&59Djjhhh&l+ְ~Nc,h)nj`ԉlvOTR)LFbWT`0Pv?1Ӓ{<+زc/~,FGҲr.9IKIf`+ҙ>e<:].q\iΖ`mo?xs@e=գ?x PUFxнl2غs_Rn~I {׭1zmnSZvf3|{a^\.7fRWxkë/]SS˅<}da<ă$'RYUMTWs""WCyeu ֹ-Vf|C藖شm7!#{׭Oђdf3W[jh|.r)?2e^LHAf1l6;d8!9y~uM-#DGSSS۫XM47ۈߗVẁ~a2m̟793xlÀфj%#}0>xGCJ_\X@dH^AӒ[=HDDDDDDD@5rx&GO 2ʍ߳.PX\O_yM|Я``@yNMv1l |Zm}aMS1444 Џm;rjj5}2OZH}} @b=̚>1#3XvȉS)o0:G{B|,9.u&߾{?Q bx5Z$%j;ĬV+N_Fw1Τٽ0Μ5S'cǞ=EDe_'007;۳ :1#t ޱ\۲upJxx0'KWRPX̣/Z斷l6I,;"Nw/q#~~&77 >pdtraXG#`N'~&S짥2Ϥ4 bF+/>s=\NVk눊tOKt9(\AQq)rDG~c}w#]\MmߵmDE-N6c~ 2q{@oc{<͐F ՗[ve WN?_AaQ /=1vT[WOvN.?*XK=ml6l63CxDLtť$'%U;;'Ke3АEO͋˅.7ybbhˑLFDDDDDD_ȷl2βh|^m[o1}xG|\Lۍߏ3=Ϟj%MO|vo?uٶ^Fd2a.ֱQF ^tT$&f:/p1F-7' ǖ47_ٜ")!b0cmZ+m (**͢=KuM pn|r_XLB|9^:v)B2xG>9&|86sU+"r5c49q ¢6n}7fA,Y¢N9.7a=:u5R\rn62{ CX~ |Yv;AY]ZvCfNkS\KvN.. JIiswC$9)ߥ-%9Ąxv9YX1k;`銵|d9.x4~4fS[WcLddzZ +lbÖnNinnv7ͱ^Nl6;c$NqO%WPĞG9|hL0"r.t:ٵi}:9VD|\ &\Q 6ؘ(nJ}}=CofheЀ.҃o̩l޾U63~Hԙl̜Ԍ|FcXZskOF D3oLmڎfc򄱄_X̪u0q͝iٱ_C e2]uА^||z=LQHgۚ?osؽCL~]AҸp1Ay.l6v=3?mǤ cAlZ8^o^#-5l-tko:#2Iy_ }b6w:@JRfn80{dr 7v)O &)1O '|.oc~7&״6[Qq)_݌ FŢ3h@?wF _|jBohd :Wye >lryN|oQyE-?c̚Ƃ[3լX7-v;{:6{u>LNdޜ+7=zy77vޜDGf69|3=w󚫷sgc0ؽv (0>) O=²kyeL~&[va銵@ˍz<ɉ <|| x} #/ZÿzLz=eڂ|+#/O/VBC htZjjcGe0u۞h4t:Voʪ[sˡ{}z#;2?rBO^B1Lhj!#}0h X, 9yZl6;U55wí#Aeu5ёTQYxQÇwܫrQU]h$,,ρF-zپ{?o?~mCʪj͝N W5uol`Z "(0НrohFLtdm6;U5 s]kAXDEFtytU/2B}}֦f"#<+lk֦& 4hu+"r=r8wi$2&8Ȼ1_3[ϝ=ۥ;;y.Ak:;>!P;""1?ccz>??S竘(-ϾXE~A1ndTF:iCĶg0АnȈs5QDxO:ma0zlK=y-z;^e0:Yѓ;2f3fU߯qe+ײl%̘2 Fn(/""}2n_H}6w!LF >u1#}5y뛆iC니}0WDDDDDDDDDDDDDDD 䋈\G(/""""""""""""""rQ _DDDDDDDDDDDDDD:w "] !CҔt+]JWOˤtEDDDDDD7}~gcc';{=w|nٯ]zG[>ՆλĨ׺""""""""""""r l}ao.kg7W׮viZiWz /""""""""""""""rQ _DDDDDDDDDDDDDD:@uD|"""""""""""""""EDDDDDDDDDDDDDD# 䋈\G(/""""""""""""""rQ _DDj;RW_Uk;ۼ2\_z2|m^b J25Z,\﫷Nw>9Y7ou""J^A6f㫵Yqk&IotUl۵S'tzKedFǺfv=HAa1u $2jPFܕZt8vn/~DppO)(*`0xUr5:sAҺ\_r1\}l6.\, %9А+mnq&scQDGEz_HQ%4'2OsB)I L?m\*ѓ<~4}ڎj%ByE,7O>pE2vg]ʽl232@s\s"""""""" 䋈|hhxRYUð!7|/)-cTUy?x8vcıưŐ_P;|1#xx>ތ5sO12cuNIiռH#ҲrZ d`™sx;uQ<ϗ_ںz^bg?E|*j0vTy;vH߲}w 00ؘN6mg̘:~6fԉ^(cظu'O=!2i?7M#6&Wڲ}/G;ooǸyִ^m'!.O^`=<KuNDDDDDDD ED!kS?ZJrbu;ϗ\cMRb<Ygα|**緿)``ˎ=lݹ@3pˬ`]}}a$DzaN^|Q~>bea_𱓔Wsy&OñY۸wu8lܺl]oa1;kmdgb!.c?3O|j[c60jP.tNFn))-#7ǻ\.Ǽg|_HH0 :=z- XOMI&t6gLն2OGw/ӽ]6ȗf٨#*2ܫbEEe5] SuNDDDDDDD EDbXy=N'`0×b᫵/+/b4\QIUu Sh`4ٲc/7`13#2ٰe'F[fOZzq7ާ$N'[weqpYw9K17;NvN.U5^lݹТ;Icۮ},r5ɉ$'RS[GCC4f3ť,]wr((,fGK?f3Nf}~J/]v狀~wOg2rd$韖 Q^YŖ{yxg1 8-=۔?-wtq VHZJ2_ )rԌNs7nE}}?(+p'o0sOeDSs3MM;=wފāٸu70}KD*s7ZMͼ·q/-OinBZG[%+H̼gr1UPUSl \/S/e yˎL0ˑncUl6v9qCs" ?{Z"=F& W۱Zhny巿/Λ 귿d4aۘ?osfN؆o)& JF`}niB|kL8ų?@dĕg]I -=𭳛/(,._ko^vNbؐ@K6oÜb4ihh$88S4v㉇ĩ33kd:1~Yyё61^|Ŗ{5}2cFfh=őHiS`0пuXr.\$L;}~"#xxB FS=g0d"##Xj98-;`0 O=?$8,LvmBHNL 1!CNu&# wYO/$)! ΞrE%.SYi{4 Zu_IMNt p&G?wo)%HMNd}Dp\ 3b9!2; KwW&o^~$.^d9_,d/@jJ"!!}Nؗ\^ABJRHm:;@v˷rS˜E+*.q_">w+9r<iCGORYU=NTdhh4(08( 3?͊53c41Fq2 WUp2 ?Ö{5cJ|p8Y2B^홧!aO0?Mͽkwe[x[tt$߽>g_"%)!6gãgjwջQ @m\p8.&7M}ӿIClwQD{J]m ̳;g̜3gl}N$$'%Ny%~Eݴ }|" k6lǸ#u}M;i6kJw fX,Vdgfxg  q0igLf`5b("#hP[>F+ƦXV\X()q8s{ty=YXi*j5d*k1 ;YCBڀ|O2ؘhɞ4sD\{0v$F,R9CŀH}zZ|k=%ka0467N3 Q~}@EֻΖ ?7=cQqya8nN=y)²pELWXrOCSs T*NGȠUwED=q;<4sOޡ~n ;QSW5q0qܨ(Et3nܺF- <>NVo@CCx[u{Nf4إ;nfჵJZzgLa=WWcFcXqpx6!""""""/0OD޸j 9f9كp=rР#NהݝR&QQs#91WmJ|\kvl6( $'%hܻ 'p8ii^#|g#.߀knǔ Hd$A "kP2Y`;SXAaXe'^ 9JzK=M.by ;i57"tnQ>@ F$k@Ee5z-8v ` $ P"%;;yi""""""" wHDD]Vr.^ z б.uTd$&-ڒ^7>. Ky_zr=2d6CG)쾾}َQ# ر}[[pi vLS)Ñ7oCo0}CDr{RqmX6wҲ$={w*ttthhl /!>O,}e4a=DH|pߥpt㡞?ɉ hljF|:d'L`4d68ۅ$ 9}ZM2;JWÈaSXZU~,VQq2+sA*>9<j5Vۂ*8ʮ>?u`ˎ'm>f 5ysA׮¤#-V+N'Vo؊ҲxlC0̨Ňtwtt&n}RΜł>9vVߊOWw|),#aڦ30mz=x96lم{n;j;unTNlv?j]\R枕֝ >v C󲻔S8mBDDDDDDD|"p`]HJL&Ci q1DB|, ^!04_+FM<ƦmQ8b(N˥>y;iXXvdA>@U3MBB|fOmfavy%6mJ[.\Fcظm7 iQUSܬl8}S&};5đc9xZXa+b~q8}KƬiЈ{ܬKWP^QR 9·_T5xO HJLV6 yX1'k0&c0>,6#} JcV & 0Z ?;s0y'>]'G\l%$IBm}r9N'-9 >[a9^vy%DQD|l Zz8j׿p>z q!I0;h#*"sB]C#]!^3Kj/R^DԳT*%/!"ĜuZ-^{YXLJD"?7 Mq-:/erfчgsЀ~~6 HlpqSA˽cF :~8|{W=MÏGl6W0l-z&{ԙ (3z>ܺsO-a6[;@d*(J$4mXܸuc(@AjJjѿR8nݏgd"~k>[Ee56n DQĂ1h@;hš Fr1oδ.NQxW" ??Zs| 7g:^{Xv3oمuwps,MfÁǰ2Gc/oZF+r |qǺOB|\ ؇p|s`>kz;kA#'V?nSʦ&'/>bɲH);9po/>㞶=-%/=+m]4 >K̘VOkقWOg\}Ni5P$ oEKk;}İ\i_6sEVm؆;b=\mԽKP(X4Vo؊s%O?냗_\<ģ@Oӏ?uvҕ29 d2~wu'΅(SQF,~l}O#:MãgkpYuM>X9W >)1FTIcqN,_㾎 =wz/"Yc 0z0 j" yKKIƏ2F"}hL<ϚS'BæVdb`Aa0wyX8O,B[Yvx內B~ V75So q HEog4\o P(L&*vJ23u nbhjndFRbOg߇hDcc3bb} 'pڄ#'xw|g_&]/Hu3L%rJo`XV]nhjiN :­3݁fĸ= zI("2R78\:wZZ ˃vhljZ$@wAŐk&fh4P$Ao0j!>.bA[1Q~4@u_ZZP*~蹛Lf P*0 u/"=0ef&·3ї o(l==ҝ~;lRGiC{:<;B |"^L#)!nXQ/@>Q/@>Q/@>Q/" ":)--dff1Lg:әOeb:ӿDDDDDDDAA )z{n}wwǾ#֑'d2*AD_9j0,.APo{{;|w٤N%4ӆtxw"Pϩz򉈈z򉈈z򉈈z򉈈z򉈈z򉈈z򉈈z򉈈z򉈈z.5uر P*xчзO.VohD\l,DQxE OWah^6 ?5Tsk9^}-@kqDDDDDDDDDDD_V gw+q9<>%IBsK{ƭ;xy*jѧ+a45CҊbX^61sd@ӊs%ѣ]|5fw;ꝺ߆˃Jb(>]:sC3sAa +q (o4#jSJ+$I0LP].`Z\. ÁOWL&O*bc<9!ysw&v:]a`4ӅftBRcp8rYB=SÁ6:67!4)ONAj0M IDAT[,ӯO*Y*pd6C NF  "d})d&"237LP*Agnj@m'IZZ۠hP;; /7`ܙö]1QZvnzZmP*C%"""""""|"t߾.Mo4q N/|Я+[^Q| \\z 7lCcS3DQĈ<лN{j;Ifvxmmzo>NїnGzz$%C 6&^Ǖzyc̘2}Rin.BZJ~ڷf6\.'En>z;\&spbb@[J#ᑇftzQFw%jí;xxTd6#1! ͰXpzL$WZ{k& qul޶Q;{Z{WVC#kȠy{:孭o|ixǐ.;^oOUJ7 ;Nr@^yiw~qm}/`2aX#uZ y{z$͘,465#9)},LWjڍ;0fT^ӰXXy8N/,F\l,,V l؆e+V w^\&8z,o p7^*+jl]EgrAw# 1nr,] ~N9܂"40LEu xe/ F}c8~~e 2?W^x }RԌ訰Aޥwz?JH'=**O{[w9v 3LQy(ѹ3QQY6P8|( H0,@{p,~l>237lk𝗞 ՆzA1]_'59I4) ,yd2 pϜ:- -A}!&&6@i wp+_dzONJ@xφG5>}*])^9p|gK/BМx'CCo0ZD~7.?9lDDDDDDDDDD_aQdž&d L'91px>!. ?®}#s)I u+)@B|X8ǎ>>S'xIMN$%J1neH7J;z=3MNĝ 4;*5=egf ;3w=ExxpH3J._Ŕ c0}8:kÉd%'(>[FsHIJ `38݀$IHMIj PS,6+ȝXlݹzC`a41h@?WݓaXp ںzdgf|y9HLUأ!%9mztZ bKWP^QR ٌTTոf$̮B>i)Zm(~ %W0,/I0>8N9v M".6ٙ] `9XrRMCUu-k.irL2ϕ`՘5m}OR5@w3T{ mw(">6z=N j3Aw0uN l `ִj"z0mށC' Ƅ'`;`-7 S'uҲ? L2o?CDD`m{KT*aX1sjLDDDDDDDD0OD 5i:/M۱djiܟyQw ;H8hl$%aۮ8v}пbc*kixXqN.7J.$ahims1GESXa6؋MpMCj2 XºͮuʇfC0,/PQ*xLe2gMxor=# 1gdLJ\M~5{Rb<͟wـ@d @vf[}'Xh\?w\=jV@d3>އ+F!.6\@Kkg @]Cti)xXn xP]]S8#ۯ'Cz4<:w`ZV\z z|/}?PcG X?:k/}F *2 ͜GO?M--j: >E}M/voy|v.ؗ{w:L_4+V eFLf34D`4d2#:*uʝN ~=}M>-mhmmVE\ltۦl"6&'.$Ao0j!>.6M-P;F&luv3d҂('xQpMҪRAwxN!/IZXDhS3?t~ A.7<= `Y1ⴧ ZMo;wnj`mgXf@lLT `5LP*!;?}O##3&u.yI6c'zekBhxo.jaya':*]%HNHc";.Bk+r$%ΣVV_3T*X(s=;A@LtYEG`0"6&u pj(m9bCw nj`mV|ֿ,;:#Q@>^zKSWVAJR]QqmV0h@:fLH*у+S}|]򉈈z򉈈z򉈈z򉈈z.Iii) 33'Lg:әt|zo,әuN'""""""$ nOcs식d=靷~mzH($&ġ ?MDeXpuL0枔/ox7{[+ܭs?d$ SX("Rrl6\y}Ғz_-$ؽΕ\k7KMNĄ-;"%9 # =y>Μ˥PS׀HOI !4íT/|ŋa|- # Q}~o/D̜:>Оw`bpq:xgt *~`I^|"dYuXؼmoم(xu:%Dta]d6j"22(tF#K LRu#DQ&wƁpf2a1LP*Gr;fh"4$I0Mi5ݪݸQзOZkSVMDd2Yb;`Z`p8gLaP݁?qX8o6`4pbXkwV Sr?{PZv~dgf>ŊW_ZO IV$ z:е*LI]\ˡPӌ0cnu.^)CCc3P(iu~d ѽ@>Q/R*أNe5V;N[j!Bƣsgbqu\.p:|CsC3nځZ|)bcc7HAp\~GЯU5yxEa;X~ jj)IvVۂF ,^4Q`nN"/; lڶGOjVB/F' S߻~!CXy{R[Dww>B{~}%◿t:D`I^K.4b-r$ HMIs"5%oyڸu*j$GƌiimC3SXc'd6Ah4ab5+ɉ xxTXWʮ#k rXϕjb#1cDL3w:%9p{l@Raq9ub؝ZZZ?~ 9YzN'|j̘23DD=R)33ܝechniELCDDDDDDDDDD 5<k6nGcS b:x)Iw+nFߴIK{gNG@.ɳع d Dt %JW^44ǼӐш[vaO;lڰvUW4,V+,x7>Az4cHMIrHk·п_Λ&9p,?E-m0 xoARoEX45|8t$̝Q[׀v]DQ+/<ޗdaM-k/B7̝@Rb;=59 ͂\&S~N듆tOV}eL&lܺK?[ZWp}rRz0uX\vsWELt$&Oc'`tjtJ{LX5~ɪ:MãsgA7Xa¾ؾƎ8uv)(9mbzz$ zv=]bdA>ƍ\W1{V Ͻ A'"""""""` #6& {ԉc1|h.8u$` c6<4s ;;yu HJGkuÑ@YSޒPUSSWɄ>):&IRȺ=pW_|=9=paJh5|b.!7{W#9)eoR,?EcҸQ4~4(9d{uGߴw`PBK{AN<9*U5ć PXwH#"""""""ރ|"^hljjŅyܝG45FɥR?uM-PTp8?5Μ93&e>put<))5%YXrΞ ݣé[EU s=UVb@z?w` u̹2v 8xnYL&"6&mWocfZhjn _n T*%!c&Pclav.±gpJP(=rH@Nq۾ߙn}p [wIy!p446#1!.k.76n#?LDDBN 鋟vV :,|"^r4ҒF`F\Lϕ\ƒe1n ͅfCEeL& 'Npb)d X IDATR{Pr{KaxYZ螲=BSgKpU7V,7P(E(bB9S¤Dw ?}&;gjHv=cc]GwKuC|#^;=/e7PUSCݝ:$'%*!V""""""""]'"%Wi9 rq1dZ80k\R8,^4ϝ}/0o4$kXh>4r W^GRb<]GN`Bd'inSg.r)O'-V eo!c`~W `uۻK7 Uյu/[tJgk'-M=c8|{W=MÏW>M)IG~K$%cY;#BBd=/? 3R B%׸97j; sgM 8t"j'߅NL&̩`1R1uXܺS6B^Tza8s"&5zOK?HEg#**2뛆_tZryGHKIK/u[w HdzO.̌){F\kX^NXuGOUc9Xa+Ε\<#SX\L^V݌[vaY~JAe+K/=X,%Ǝ_T`sD>_u[ѧ#&[-ٱlOWfCE1Ls5m"l6>EŐDL?Or1ٙxor͟wȂu©Cz2,`YáPȑ j ><-Y?04/ px~rcC UA 9ؘh7¥RDD1 ϝn:p DD_--m(r9ƍVAJ~mX+W@~B\ bo$"߼6j Hnj) (NOD_iyj}""""""""""""꺯 @>Q/@>Q/@>Q/@>Q/" ":)--dff1Lg:әOeb:ӿDDDDDDDAA )z{n}wwǾ#֑'d2*AD_9j0,.APo{{;|w٤N%4ӆtxw"Pϩz򉈈z+ҩWK,w[  ) IHHBB !`m۸ \{"ӞfI+z}n;ߙ{>@@@@@@@@@@@@@@@@@@@@@i;%O?w*"                        1N8[{W9͘yr +[LssS#c>{lۑg8na[@}PP;O#;(mF475 x[;Mr@q聱댝6=c=jTѻgS}2yRyAqb%N29?䀘8a\66613555388shlljb1e_B!N?x1ex>MM1nhhedQy_s!1aܸxƬ}6PySSuϊ>fKmE=P^jߟjY57ٵOK.:/ K,GScc7ױaƊ̽=$v.ӧ'CP5=O|?<7vFP-OѱvNy1+WgKFGGG.qA$(~7n̐ۘѣ3G/wڔb=vgؓO#Kg~b< #͞WژqƹgK}{rη^'Mc}!nXlE|xo]qi|3Vq.Z4ϞO#㆛/momkh{1~\O%촩;i'~w>{k{ԡ1zȚ /ce5v^y-n~/2oP+{(yTs\W466G>yN[`o />+#fS}#qɅ?.w;ƏXdY#֬]׻l%ΨQ#ci|J 1c),\[7 1bDKld0rDKDDcnj EKX|E,]"b=9v>5""VY 1u;ft,^,V^ÆŨQ=qwWW*z/>ω&V?6>ɏ~3 GuhsC!Ϟq 7qՕo>uFhiS&Gdž `ظ3_SSc̘>- 侌76""6lsZ{(qΎ 6FssS566) 1rh 7FPD!|@ccc3fT[>/\\&Q/*|.k><}F79Æ5Ǥ cԨQsllh]OpQfN'FsSS,Z4Ss^ɵc#Zbӣ+^0Z1&MƎ`q\ֶ) ׷^"z>- B[(֥5ZVVehljwwѣO>\|Ēcذg?>>7) x߿1j'1u>?d\~qʉƻ?Wѵi--KsxgomWb%m?딸{_x귾Ko}7 -et`$nLr455Ï>٧#͍7_pvxW#"?<w}/~]y臮g_5{?3"@T򻋈{/=5c׾=Ƿ㈈Myg79pq.ÇEDD[[{tvvF[⦀}#>Y׿hkoӞ{:s52Çw8#P ;:6?71j~N7V ܻ;Jnk.>yv{}٧ƅ?Ʋ+cXss\Ѓ-=7=m쌫>>7{R%ߟ7wz?C<>Tuf9S_oƔ& ƍƏy>|6cꊇR!H|[t7#ύiSv_ Su[1F=8'nΈ(} ?+>_碭=Lohhh ms jcqCsW7]]1z?o^_̙SO<6ZDcO>7hY\x^Ŀ~[}zȜwt(O:""⁇7' V'N8?JL?.9hxbZw,ѣGŅ]|A<+ /GD{zǟ]wSL0..`VѻϿJ\{-foqy޼ _v,Z, BxQq٧g]k*=Je>fuy"cI}B);MaÚ _v,[2F˖ж}qчO!hnn]voe-Q'gΞ'?p5lu +;c3  یaXdY~fa/XΘgL˖ ϳ{ġώ}0~ymk P(ć8S'DD/]q%yg?ů;7s\pqTyΞ'u7 _yu^<܋+bv9"FgXy Us!q'mw>qs8aašba'W]];T|t8zCӋtn^6Gdž kc5wHzұfߘMU֯__|09иއbԝC\sCttl[c葽^|yQ(kߺ}Oc>{:5nwn}k$FĂ}^}+V/n5Kb]v,Cv߃s~տj 3 h(;PΌJƒ!}WwW44>{=7y|3[x z=蓱q8{g -7s%}M}u#z8&bq7]#;(sSuWwOWH|qy k?{b;`o,^,;bqm{K,Y<^wܑ;zqRov|1z =vEEs*9u3/tQqGOO= q1}--#b#6oa?i+~U*sm]' aK4q|4{V7&֭[7fTkǜu,I\x^{ɽl؀֏yF{y<܋\ruDD1ϣFyM˫+ IDAT{bvzoؒZzۨ#bĈ:ur!׬]ȿidȞg~4l 7rq qwݭmC9"ZZ#͗^&<5qOV|qc*M)-Ym1iRD\VﮐN357><鼳NtnwtĒ%cذGb$F`5}%g_-OKSu/4g?T>n#"ƿ}; Ή,.7nVJ^'s89>␸أ~7v Έ;fLvrϣPk8Cb1 ah8#w{gvbX*&O_#=0Kۯ[;:['gc%cƌ);MOmJ, JIePhhM+߾ Ⳳw1sw+w]vQwڷ֝ bhEƧߪ#bcgg =D,Z4wť7޾7o_mUIŶw1X;;6u7߾WGn5ֶ9e׷M#d9rD\rygK_n/>;"ADuZ\k7g;cF5mnAeTb<<67yln|]+V.9I5׷j}E{ߟ,Ͽr6apqGqHsC߭^WǍ:6ƛ^fo[49bXs Ru \3NfL/}31fxas#zk˯řGvP,XzgbÆqg ÊսZvI\6n/}ѱ!#sF{{Gum qEKWM`=wN8:~umNo ϿJmU7bDK'*x. k+Vnm?y 1})}yghc]wXv]}666liذ!^|o`U[ty466^{Z~7vL}&OB!^|ܠ5`5 /GDĉ5Z|etvvfތgO7fݳ)yT{ȾWsw^{V52nMM{\W뗿2"">/o9?uO|/??x≹Q#c~g>јpqC/_*~r q%o?[n;^~( 1eIqȁ~ㅗ^{x$>x[ΏG{*N_x^\:n>=bxuނyꔸ gEkk[~7|{|}=v%>._s}-ożbW\,N8:^ ַŌ_ޯ︱cٳɧo=>]]q'ť>x|3׬]'&G_;44?c\Uɵ7ħθ;b%2%vuz\wչ--ws_\:^x>Ɋ]s}8{f|wX-N9񘘺~mm1eIOĈ8{ןpQWQ}&}̶_z%?mx0V^`NcDDUx٧ğ~qO=*#xm>|X?ς-N=XdYhh(Ĩ#k۫=U{*>R<qIEsSS<,VZ͝T4-[xq텋Ąbu(VYgrBsO?B|~m}ѱ!.g2_]Oz,/Y'Mͷ.y8Ⰳbq1b„qqIV\{.,"zi{7.;Oٳ鷭z,:7W2<98SbÆk{P^~-;{ÇEggWZ^z5 _+.(k#A~Gdžxmo--]]]1o/|=wť="z ^[ͻ{DssSLSʷMޕo= ό}[m7|g\t`y}[W%>N~ѽ6l'>ַl[,xm kn#=0=wEK>c~I Mw]ݍo>J|ttl@}nj--ã-cnj\P.`.$B߆|Ր1ߐz5&Mw/Q--c)tيC3|e˖VcccL:9/_mm}76/_pk\{1eXfmn?WwO~C{Y~ &+WURZiS'kƍۯJ}/?ÇO} >,&OֵƪkjvJ7vL/555ƈ>g &(bufM+Uw6n9%֮[k׭y>|XL<)Zb5rqccޞ'S5< B4yb466ƒ-ihk׮ }oB>|XL?.֯oԵ^V=?jnj#Z2uu57>DDtFDצY>]]%^ݩD[w#1eGkkk7@wNgggK[hI1z7vl}N+|'ļc19=3kGGD,[rޞ[^KF=w?!,}Wji55}`>}^[Gdž|-T9hoW-(g=S5RY*Tjƍ!r+j- vAyo~?,\h| j۲=wT961v1}F,_*)mFlذ1/Xo* 1mN1vhkk˖WkUr}홡fuvvs/\φ _{wwwǂ]@\m[C.":":":":":":":":":":":":":":":":":":":":R.ݡP($gs6$擯t֫17=ݔoJ_>twwuEgV(1"Έ71gc+cī;5ݝhNbS{$LP"                         Ȏwof x[>+- Cl\^\9r=NbT? jRz5Ui&ߪP̖΁Co?y@`3-: {P_ɀkoZCYm_?UB3=MɘT~ՆbhHp^'o5P(`wcS>z2ബxV״;y" +7LAV߶X ?D蹁q¦Ŷ|aӫ11|SDXJ,+1LLwmzۓ]X=m9MSrߪ z? \N%Us#J m(">U 擡~!,/Oj+1mK]|WWoZ}+ & +Yܝ y8{[~R$c w&eeicTͼc47CWU<WljC&ԩ]Ko;k\>]!͐:/ VRИW1/g(/']|kKU}_`:/Џ_}+K`{vgU '7Umq ӕ鶬M/KVjyVpB?k:bsE~2/*r~z`{Rj=/ͪO+Ư䕵yX[ZV;+O/wEDcj>'*a~+tlޔ WRyt{r_\~t^@_ Z?g=W~ !=o`U_H%Y I۫jd[2dJ?+ +/WwYSI{M U%+O-GbU~ny~-YY_ ˲ ^RA~D @?,'hޏ*˭S"YAސ/ACbyp>"?9U_*^wKUgtWyky2/wC2OԲH-+JP*/ug, KMlO*K,8//5~|uR_1Y|2g ^^%~t>9l d~rrꗪ۫P+9L~VU~2 e-5Yuy}VB>=UgwgKO0?9\TE~D@PT/5WWWݟ_yq杋8. B ?b螼! //n\E~z:^Yۓ*|JRY}Vg v~%oTxZTy^Tl_lOJĺŰOYYS?_*؞\/zuE8]Q3t9g;JUA~>2Bt"60=\r|dLgUgi|`{^fϫONUїB?o>jۆ`b=ut~2OWG-9~>Y}_*O_IE ؑ KYڲ/U_*?Xp_P?>,K!5~1oa~}ma}zH6* K (t rHLwړ!~󶛵<+/u\Y P( KU !#VOj2'}o ^N-גZ R}^?'sj}["?+vyDɠ>Bj: ?'۲ԏԼ*|`GgɫzϪYIrXjhR~:ϓU5~>9]\?YWn;R~z$RA~5?o:>3ݷ1Td{>?]uߐS/n#9] ;OіA oU%waA@*#o"ȯ&ϪOJ/NzqhaK G3 [_Iu|ֲtU>o}˺1 :ԯ:. Ն?+ }0X=O}LO MV]I{d{֐*~YRTRwwwzh>k:9$~a~zxb`_HM'_9eUݗ J+/7~.ySj&jg _j@*6ӕWzK _.۫jK K_:ď5 Y=o>tt`#kYSͰYymۃrA~>yy^~WƲr|0ڳ@ՏjA{!O(3eƯp ۠j|ԹIDATK~yY|>?JCR1̪WӕYy-RU TxgdXJj+,a #j[%]qn2?-?]S\~`;c: є Y_j櫩;!}_n#Ct{V|zҀT{^{B>6@?kYlH jh/7_j߼ ^JrlJ͕˅OSF.◛԰5#4O/+g-Z?uk^Ug4`/4Z^*!~ĖZ?o(t泖u,O>>=~z=Y" "?3˅lܲ!3A~:Da~ $wL;gWW:~<&CGQJOF!+>2UK-"uR_}+iQ_ ?"7H[5a~TRUW\_RvJSn[B-.8]i}[o0}% س JkYPA~D|z4P~PY^m~>C,q^ ?d_i{%a~|}CfVtb[@BZCG$j&ЯdWUe{[Ծm?"i(6ZF@AxV{w|R!՞O_;^IH_"W kQ_ͰTU]htj]L?mMa@Cr}^{5~޼p! #-6/Y @J-U_v2ďA~ն:joU_mU}U'm ?0?v~ֲ; r@b!~#fYr Z _vZ_W!~V{x~-WOhP[r~]GTvJr=hH=[+ďA~N6ЯO-vj5 5 LVt٦ # U_o~QI{Wll:5O&A6U04jho }60/V[}W[$ւm>ϲ}m=ϲ]-cX_%:ҰwLuDuDuDuDuDu&i[ IENDB`circus-0.12.1/docs/source/for-ops/web-login.png000066400000000000000000002334021256046442300213030ustar00rootroot00000000000000PNG  IHDR=~ iCCPICC ProfileHPTY{ MHɱ4Mi$fQDD@pȂAE 2(&Ƞ*ک=_:sλ@>` |g0  skhvc8q$ٜ‘V"§US@9"~T,# "1\0ˑ|d.&aDOf2?-萄sٱ\HDɳ9$&&r6:o"M&3Z56?-1A edSgkFzb,p TgMebA ,rZ$oQ<7rH?%lbCqu[`~(>%-m1e1ɟe9 9&wpRM 1rD Q|E?&_/$d3]E  S`Lq*'#u6y$^&?6:&eLML}]H+ )ЌAܢOb}82P j@=8Nv.@Fk0 >ipB C  "h lh;AePB>hz C;+ɰ4k`:{ÁZ8Nx7\ Wp|߃kx P$,Je\P0TڄCQ-NT/J@}AcT4 mE{,t2z]G/ѓ F001јtL.S9|bXmn`a[Av qv8_5.nFq$ s%FymK4AI!؄LB!(p0J&Jv@bq+BB|B|O"H֤UXR)8i,E#nrBhQ)aTnJUH!,V.&v[8A\SI|xx) -  &rCSTII_DF>1)[*GFEUPYԣ+Qi4C:N:_LLL9,JVK! [({R%JKpZҲOrK8ryrrʷ?U@+)RHW8pEabRۥyKO.}+)+nPQWRRVP)T4,\|^y\bRrAMDK.&UU=UUjjAjZ՞Q=*+454i414{5?iikhjӖfhgi7i?ѡ8$Tuu҃,bn 0\j!Caaᰑ6v74-ۻwc ƏMLLt33e5m60{ko1?ljbEwK+Ke帕UU]G/_X;[obcijs/[CxF۱9ˏ.ScU iGLjlZǗNNqNNog?ظltvEzI=sWsvor9Pb I/+^e}|>+^+xRs%we/e}**U^gP4| t, |$ nR"\l7BBcC;paaaSk_3n~ڌ}%;^|=sLDHDc7/9Ɉd^q2.(j,.z_xCLIDKlY8ϸʸOu3 ! Ĉij\)n_}w=~o;ޙlۆ@ @ @ @ tU @ b @ `CW@ @  z*@ A1^@ @0!ƫ@ @ =Z iZ@ @öm{_˰?A;KO@ @ef~)W1P@ /ahA,@ b~g*ƨ@ @ 'Ffk/ 6P@ ٠ ?x[CR Q@ @0[-!;,ց6p:@ @p` l8`3bkOK1H@ `DOLٯ }j_Fmo@ LSì/ˆgkko LTp@ TY[o մ+v>0Z{chc@ @0諭1F{e,;k/ l⾺_ @ |90o V zW4(xp+U N_ @ Tv{pR2`xZOVge+@ @ +nSg yjjkFkoWf3M' @ /'zbll+#vfᚍњΈi@ @ 73f&nxɁdd#4jkFk+y>V S@ @";!㎧csXUm8USi.nz]WkUޮ|f/ӕڞҦ_ @ R!2[!Mn_v]Cύ4ۅY]Uӭ/U>=@ @ xuS\}lldžkFg{2I+W\zG @ gnLW`SjX6Fp8u9kOe'.@ WxgFl&ls}Mm5S㷷m01h@ гPt}bj5~9XY@ "\{Lk+^׼4l~*'[c.S Mefr]MTr$L @ |fi.aʧdz݆{jr쥓}kO^ٮf8X6,@ R!mÙY0nF}qoƪ^o%M=1\VZSz{At33ݱ 1d@  >Ϛꘟm h@ >tu+9{uWk?VT3Ys@ɕW^yĿA-ͭTJ @ 9֚ח-[UW]@kov] W@lu3F1Z8+⦃/sم_OIdzա(_.EMx5*Jӫuh5~(T*wVnr__/G> ƫs^F:2w;DWͽx^G=v3DS{555U=*TӮ:|U|ٴ0r& f} 'Vc|r|}ҙވ0~R:2)-.zͲ,nʏcݦUZ-T4SO3`$@n 7Tj<Ӥ|M7/=}9HA-tuVnfʿZثSJ?yY p_WQu|D"`087  _tA lBj^nz vۭӠTXwWׯ 7}>XQC0 bGC#6 N;U&UfQ՛Vcſ5o᠑%3ӰZSm;0yw0v|jQXX#Gbĉ9r῵pmMӐrL<[5W\qp/_濢Xt~}mZ͐u=Ue3?2vSÕ~bx>k^w@a0 )Ttj쫏58GSOĐ֏Ƨ.ó8-sX4|LLjD") ^^oDR0)>oj\7/(qxOL ^j禦yS\y 俥}vwq8q"jjj?***O { O=9TVV:6oތs9qbܸq_rWZGyöm۰i&|_O~~?4Mi_FƎ.{Y<~܍^J 6^5Z6c~4h+yU?IחcC`DAoٌ6ta tdyJWѺaߦ|-;(t0߶`ٿV೏fbα9E_8f LSVl/b)8vq ^}c|8q7a#n^mܫR9]^m\/Fqx<vpAspSe4yN?SatϋJ[Ty/}Q޽wurss!4MY_xlIF+TL X^i6/V^Y?w3XVeS@( DׯEzN`:hmAn`Ra;>)&5 uPcL(DA^ڷG @7:hۃL(Įm:mlYѓ 0͵.r&.D mVq!ظS`4jJ; ^^fjGD)aI "wHS;c$j|)u ^^d}ظq#|Al߾EEE8p '8+$PVV˲PWW@ شiZ[[QUU;@mmmꪫpcٲeXnO;`m_vZƴiӒꋴi¿?P} Xn$1ٱ[|7+}m8Ckgɱ{lFdVL)!"к-i:"':00mp99! >D4ӢQrBF.ƶۜWT^KN\ 0b^L:y0lpS TyFFO2PX7js*xIeAH*΋wQL?׏srˤ)Ϗ' Z躎'4M9xvҥKqgnCCC.̜9555N}aNYٳX iO<?ˍZ}H$+?/ӦMO?믿7pJJJDZqFCرw}7ϟc3?n)X, 6 .CII vލ뮻K,o~Y1$>y?xgPZZc0 ̘1{.|>.B|G8#`6pwSOYg+~!̙Mpw?/_~^{mR// ]ۆH$#ecѱK QnϼɆ׊+?N Q"5MC(GA|ùBa  >ޙ\ l#Bg{STPa|04`:9:lF8יLj "7FЗ?۲th>q;= Hw>]']x|IQ;=4uFw,.ojǩn 'NĉSR;)Mr_xgu⟗+O$qm;i@+{.&ND":|g8s0eٳC q VlA+8˲\okkC~~~J_u̚5 SNaXd w… ;\veu#Fiرc*++-|2:q%mz @7H$0uTvax0oCoTFj^n`鿺X6lY3J%'Z~"CVK34 Q 9]|&tt }9 ,D3نX @"blG;^yDyFǝs52:j>/)7oMܼjgBKޙ/ ]s߭dtʋ;.(\Y%Ç먯GAA'|A v ]1qDq)/p㵰W\q81sLWQ__F,Xx`رhjjr*..v:˝~T5MCAA/477cܸqD"Nَ7k֬I>!O H v(vXD0(_q)i0hoC!39:JGMGӮ,S);`u$P:jz''cGL߂ G#/PpK`q[60~a$@0Fpo5 _t:7/\ $#_yԆ.QʹOyyRTw<-^ƹ}qq1;`s5Æ /ʪQ9s< ÆwbǶ]r!7*#U~i'`8htÇU۶"+Ōi P\)oܔ6]\;!~o'qҩQ@=uwQ#t'{T&<$o=iO^H5pt'x:S͋x)^O:eHz?R__YzS 7xޘxu8e%n[ؔ77-$OSm4i/>Wmh#}IqxF#ʖ@7QO|΄*jeˋ&G q;?=^ΜN6$;|pyxY5Hi~:X6g&ewp)^ pe$5r. 94M0yٓn,y\c) ?{\^fdW=[xY /7>)?vw|ſeu} H ¿/ O \fSy]ٹj3lm%W!pummq +;u"WdM}}=۝ȋU<7@50>'PEШRPn46\n.3K5Nxg2marpUrj@Αw%pF s.IGiq\\:ϋ8TgjܹH<׋7X)L-SZ*CVC  0rȤ7|qٳhl`n5ߏP(|B!n"٨N^W^<:+@ \/rZ7ԶApC3֩ܞ5"yx{-RYM<8W?'Ex¿/ ¿?P+p^_)2lsÓHg6<%zp*cmU)anۉc&>1R(jjjPTTE!'V\ ?0 h* TTT 4MƎLj󁆟A0 D" ,iꔠ¹W|PuyIJ&)\W>ѡk9AʟOzS4$EyN!(j<ܑ,h삡0__$@ʷ [fp]QZWBOtQsj9;ЉX,`0H$zcE"pVe]xb+عs'FFضS:WGG$u|/{A! aXv-Q^^>_OiHs,Jy~|p#p-T)k~J O8j4Ҧ]mI[XM禹,n6Cz)^<GNN6˜]IW_x7 vn冫l X/d}@I=wVZ0\gʫN:::`Yp '*| `Y8 YtRcǎE,DQH@h5r(| bH$=z46oތaÆ9qi0%f32{\iBTz>ۋ^s5 dSH'{#}iu4 Yx`:g'n9.k|' ¿/ ??Nk07Zd;m- ۳2^VXӭq~n'hA/:'n}y >nB[F"#fp!#qS : @`Yl8"(rsst dhP_A4ȋMrG6mcp4>R$:x^x>ҠK>=LSʽJ;jyi8(L?zѸ ¿/ ?W]S[8׊koVbӢ/W/4%8]A? YN,y0<^`Kqq8L$` yw=#G"///m:``3b1=q> emxD`03MKy yY4-N4^nU9 z$;=m;ۄ&+x~?I&4Xs54$  9z%7m],N}<wo I_r¿/ ¿?kV9}5e+Qk1^݌U/#5{հT+%/eu~W0/^ L#ۻtsN!LC  h`4 1bi5hmAWРH=( 5C׹a|4)/~T|V'ܰi[IM 8$7yON'@}d ˪?e¿/  jlW^Ӆ1/_뵚Zn/pWPT]},r 0nPǻk`6\LKsު[ȣ<h0$ Le%?{DYyeǘy>J#9 6,O}?@L\n~ v'Oey͔ʓ;J_'v9VC9WWYՕVkƫƙ nK^鎽,Tϼv3`bЄ``Ʒqy^/8O?<^$7HJ|EIJo~'=#tQ&9dX/ ¿/4辈VN x`׽ =u 9m8S+MZv[~?bSiYW۾w{p33l7n\1ЀSO=xW1sLر+WD0_?k py?ğg|_G{{;>C5W̘1g]wգ_G®ZL>?q7{`eV|@"/2 cP}>CaG1=#DۧhCp aDҖ%ڦe|sל8cd"]Is+q@)ҏ QtN-ygdS|y= xy& ¿/ ?݌VWtOhWlsߥj\}egILGF V=ׁ 0F~iߏ={sŔ)Sގb,Z`_pqa0MgƯkwy8`Y^z%p 7n;0;hnnٳqò,cXG} .ĸq0o<+VwG}ksѣa&?DŽ o~k׮eYx0{lE_=jU55/#4UXV?ET9 Dⰲ V=$asPa񵶶Dq}ǜ9sas99s&yq 6 a---mHgL~_a…xgvZ'_N<Ĥ-)q.]_=.2|_Ǚg: ˗/G,Cuu5n\q;q7.~/ ̙n m;z>JWw N|4tY\f_AD&"xteuyũo'qn5*st Iz}ʋGr{io_nf2^5\-$ӭfmro=<683FQU'CIh|Vch?QF'e5Tpr)o;D\p3mMMMN۶TTT੧B8%\7on/t]9眃p8N: 駟:/2|5kpw8Ϲjp8$0 8C1g̙3K/9 qwߍ)SN9ضiӦaժUӟn+'|2.BxGPSSk"`x>(5w{AɃJω@eFlqc:Nh4v]zE}<w|i@kooG H$uQh) ueĹs"I[a:$h 7 psOol4ɳSiqn"5*0P*XL_hu3`gWFm9 ۷ o&Fn:@ҏUNu?N9 %፝ƪcF0"ف=!I&Og}˖-_Ab1[}۶{ӟp]wDze0iҤ{'NXl8ni9I.$r=CI[d}g멦zj_x__(++CsssRPɟPWW(,,رcqWbȐ!IqRq `0x(i`~|I~|;A"p={Tz򲚦M/@H@Kw~rßuKeO(]/A,sd 9麮ʐʇRxM< <|qg?s/CNj>! ¿/ &F*qvgM8}ku7kI;\I0 ttt8Whk VfMJky(((<7|xx4 O#<p}aΝسg >o6~lh---Xf .b~bشiS7Ν?o6z! ?<֬Y: D< ك_~C ԰yGi5 7ntn6ϟ?6l֭[#t:_(,HyIix'`,^X Z$QGMӰyf|_ʼn'aÆ} =<N>m)&GDSS؈={8E]]ك@iPs%ŒGG<Op.2NdphyZ|2aY]oO$2o@>S<|Wi'ӽr?aO<ku/ ¿/$>bURdu"UdjfJF*>U$>$=P[[ya Ь8`3X[:*i8蠒'~3|߅äI0|'ڈxڴi8#pmǹ瞋o[l[nmc83o֒%Kh"׎W\t]GEE*uYظq#~_W{PUV~~㪫BUUVZ~;\xXh 7WpgoǦMp1`Æ [q IDAT'ӘЀF5~GD;iMV<w.7ǚ!I^Xv.ґҤ{iKju% tPSYPyRYӄUpӭȫ׼lN)n09E|UGGG O_$C62Ѕt*ҭ nQ v 3& ?ӹ_F>Kʶ;_?)(E^h4 MpXM.ŃyyyBGqq~MχP(vף#  "''mpСCVNNrssH$vض\:FQW4MC(eYضmFp82dѨC$qAA;p=)p8H$MӐh4 ˲\KzN@eB[S3M3l~?rssBQRR9Trh oFL6 k׮`0MӜ4v&(^xsO@=4ٶ_|gF(r `d>[h Th<=x3-t/ ¿/ @hѢ+E[-`f)4B?.c걝f2ϼf *^i eYpY#ߊtU،8hRK~mahkk >H H2TX,X,p8{7s~M}SmێXVV0{8`0tQǓIy;[pys=D"tNB,Ձ"v{~3kZvo۰m*Lqȫ=agbrrr<N8i0 {giE`` t'z=Df,s<ܳ뺳 M2twBQyPqyhB]o'-Rt=ty$CSY=|$D_Ziu u/d/+=ۜvtө]ɢ@gF;5N/OGBΕ;j=MtG2398%ȹD RtXχzgi&TL`n6tɫD44Mttt8}뎌92S87ڹ 4xsg -rmL3{詌K5&9h'8hRBi$/;yyޜdTmZ|'iBHӄ/ ¿/HXe&ۄn{pO##+=6UM`[6D-Gf"i@׸ٺϰyf=xD<5,f]lw< ~߹4M/nlNuMw>f[6Lk o[-τ 儜Vv>o⚖aBte6eçw}q;~#9-K4s OAٖ}zRږͶ.8nVaB׺&2GrPZ¿/ ¿_{^ݵ 2^{9Uz۽}?CőG}I@ izkXb f4McM@)@Џ_yq{5dke>1~{r]Cp᨟Q7걟gncǎ^q.[}/9tz; -@ _6 :>Egn~|pgc[ uAj1Nc"s}Oi:@ 8ԗ[P#[O>2b{"@ @ |0жO@ ۆݮ+϶ @ +ҽl T-uX~=>C0 6 0j(s1ؽ{7^|EmTl4l!/n/@c\0۶ G0D<:O4QVV?>(/_h4#@~#GĤI=?]555C$ӴsssQVVѣG;;qܹhkkC,뗲?4 999ǰa0bk׮k'S>%SLUC]P-JSTK65:;?\W~>_=s?(δ+W^AjWF[[?#ipOit]M\zyo6 1|pL6 Xr%V\3g" rssTyɷ}v̛7ϑ'$>k)<@w~{yr U$]JKK3þ8 Lܰa,Y4Tχ%K`ٲe3gNF֍~[v5ǡ1,QMG(ߟL!'sz;E}K/@ >Bo, OfxAG[3l#d8|\QQQQ˗g{<QYY?@jo ^暵!SSڿA7$8%۶gLDyy9/^{ΝȘ^ L#ۻͦC qTP'l;~OE?ׯĉa6֯_gFv}c(:保4͈,Cԟ >/e} 4ӎ-mmmxǰj*z(o#??/2g3vڵ 8CÕ_|͔ qw#o#|w.˖-O<?z*N:$ض n砧Ofڵ #F#vtέj~_ꫯF(J{o6Mn3fWU?Okvj4Rmo'|2{*sc;@Y[ջƓp7&L pC,\.yՙ9u۷h@0 mD8$h$ddW' 12QРЀCC~{{UuoqoP xxMhooggᢋ._dʔ)~4MVHu9s|8)( S W^y~Ә2zVˌ3hhh஻__[éSQQlFގCAYY`pT{#{0S<ո&nyc!gg3/l|jq=5]{Le8\Ņh!/߉Y R"Eyԧ>ŢEx7WX\-#k$PȃR]]=Q.%ӧO'??~eIuu5[laND#C1D=vk1b {m3fqL/l珴[og:{1N:$0TPG^+WDg>xϞ=^۹7n͛x<A."&L. [:KG2Bpiw^ykڵ )%K FNi_}2~gfOSt]\Oif_-hll6z饗x$ ~?Ǐsα]G\nذbJKKbY͛7ۑ\;::1csΈKxիWs饗Foituuկ~!*[n_"˗/4M.;/ɲeX|9_3OIqq1%%%35CL0r`ĉ:t/}K Bx㍼+\wu?ҥK~`7ɲeB<\q\}qfϞ͒%KOʎ;gҥ7 z IDATHisg4 ͛K/ĢEBK|GJkUVq-͌FTUUSXR:az); 7@ y~zϟϚ5kBnk׮%pgwW_% i%twڵkkD"}oi3uV^}Uzzz(//碋.s)x=\ƎKyy9PロCěoiogp8g?;b &x+* L`kL+e7!!RHxXox<<qǓFLnJSWW7*Debۤ=CN']vYkS噟Ogg|$dq5ָG:1JcKCC>צQNvK${Cp(?@s)qW i !bnF@<0L&NV#/]8&f-qx4 Ӿg{+䭷f7\veZ'k_l߾^{-cd`͚5 (cj>//n Ezj*qΝk Cuu5?ikk+zhI'UٳW*Hjر;uG ?|>"~+2k} *{B!0L|-v^+wOaNpaL֒8Bүz<(@ˤ 8)>D"z!vV^?adHF3gN2^-N85Hr(cR3ܑ׾#vi§Nanqvmaچ*z2\0 ݴ 6֟x nV͛C|AI~9Okk=o_vˍpʔ)SWWg=gΜ|A4MW^y6;H˜1cޭ"|cRVVƴil^Tx@ӝ31nwww3a„!\La=)Т;2;-m#}+)R ϗq|ѢE\r%YmiiɪOz~{ҿg=zFp(O^'Xz|̈́B!8!@wgokJzMfCL}/Ryk}/,,ƍ|}!EÍb,[%Ks$sJ7mDYY'|2RJZGF!{}~ч ev̰ ߗ[n?@=)%2P~͛7.}Ld߾}vjz}JwEuD"A4?sq ;:999>}|\۬^rxN}קMҥKYlO>SZo{{{czkG"pD"[fII]#0\,Iohd„ lڴ^z%Kd/ 9s&vj%\qE jPu8/h0z Z#TYA !|C¯1v#/<60R#B=`a|ӟQyLxꩧ2W-BkkFpM/P}`A٩-77`0hNM6Mx/))4MFpJ^ P6memf3fƍ#ҿH4FOo/DqʇK~_J.[:o 33wϴ~gZ!}t/f=BРCCijjj z&{_~9l%{8N:g^S'fޗݻwKIII?#󛛛k ڗb-ZĊ+L$6`KBy\~~?חp8ؾ}*!,^5q4ϟOii){K.r `E_bŀVJIKKhAjKoSoo۴gN={"5ZݹH$Ԟ5\irc֤1' )l/S㛂h@^U;jsG$$q~sMq&_āqR/Z!`yyyYGL-p#5Ǐ݃2i ~_ymƟe\j峾7oBP5v2$?EjƬ&$.a2N1M 0,ߖvg`&>;vzΎ;[{C4stƺ{D3Uns%4/$}fH("|2]7VU;%nܗ^<}sFټy3{.NB0===tttse\;c >C:ߜ׳: &$~rISS>,SN{҂ilڴ7iΘ1n{=>rquױw^;SNNz*=W)SpA۶qF~I?tuuz444kn0 ӧO{GG{{͊+XbEj466RPP0d}AjBj$8pM  GNO> 7}KosAoo~}KN&Oj? ew i)npp p=?ڠcF& 8׻]$ ӄp# )n;*o"`k^^^{Sy6}ԷUN8l3;vPWW7Xg;/S__ϳ>K ݽicѢEq\^9jkk;/~ /B;9;ݻy[%3?k;:j?RXXHWW^{-ɓټy3?Ϸ\qٮMy{s(po<]1oc$8\^z7nɴ <*srr;w.?Ub8H)[b];T5Oi݉ pSYYIcc#k׮7䤓Nbԩ6&kQPPMhkk43w?XiuwGhMy.wq׿Hw?X;`ísO?4< n2jjjk.3OXx=)`ѢEA~_vmy]z<䓬Zm۶CiEEE?O#<ɓ;w.h!ƍ/^W_EJ 7`Gg>c=FQQ3gdɒ%h?NAA..{njuu5-׿5999UW]1)Un>BvɤIXn[nx<Ωʴiظq#< |+_V/g~򓟰qF`}JNSS3g7ӷA=X= w*͖vO<-[y?:u0+eʝn(-D'N  ݜZch$hZFJ.`6i-uI3$}f qer Ҧ`ob@1Dzɞw} np81n0 dL־do'xrY~=]]] L^kk+{Ob˖-Cd!Qƺ~aSÍ1:8dD6k;:k";H@ƾC)%[n)**g{mSKP;[hoogرvZB!{/3?"iH[1fŽ~^ h7 ObӦMYH$b%&L.cHFyy9k׮K/{x ͡huwXFZŋ4p \}C^{W_Rn.]j|:g9zM7dsgAAwuX^-̛79sؑS|K_nj׾5[yǒ%Krq̟?H$n[t)VX,FOOOzfb̙SM{^6nܘՅZu֮]Kyy۷>wcnIO#gmw"R殧 #a$bvHE$gO(d /NCBQAF`G"~{\U\ڮL!1_hޗH$«CUU .pLD2PCIjkkYn/vY8l-Qsxb~z`ݺu̚5któuؓ[q$ G[M\.~BC*Ԛt}Rr3fLtsbR\![ozSk_#qүORv5M?w\kc+7bThFu蠴^njϞ=ܹ"c*}lDӺ-WQPu<9e3LkF.`"҈>oj$H9( X.MƬYxihh@uN'՜wyaMRJJJJغu+~zƈi~sb#?fr.boxZI?sޣ]/-v~߈C1d.0J`tK7^ .X,f{ee+ԺtfVLH$xwCz3+c=޽{=#IyB"B s۷cK.5w`elG̖o M4g/uVGm{ymEGwk4 #{[hȔC4$lueJutz;zhg:_[naڵXniXK3@2^oBQQш> NE4Z[[ ҍվD 凉^{fnn.cǖҁ} F>2ozI S+o$鍔Egϝ;7#hhh0 d0y|ԍ+09E5~Ńw4G^:ImC5;rpjd|og3eO}}=g϶aMqx^uoǑi43Ws%8jkKˏFh#a]שgӦM̜9sXK ݻG{6][gܸq|[rپA[[ۈTd-sE~ %SpH`H&,U%R eȦ{5w4,999TWW3oo&H8e?e.dO4:XB.lk餢ci]]]ISSGufΜɴi(BP( BP|P žWVVRWWGGG ף=Z\\lg}6EEEv4P( BP(eH$B$r1m4fϞ XB Fimmv# щ6G*_W+ٮW+J a-Dbk3 C0f͑ʧWv_$kJ}zH?(Ol?NNX-P( BP("@=z5vGz|WW+J_W?8!F^_~"( BP( 8!עS?n BP( [c 6P( BP(O6xU( BP( q2^ BP( BqܣWBP( BP((ph2s j*>nQ BP( o"G唙Srغ qP( BP(hz).*4q8u4ظy3nC4S( BP(O$x=8nIhc_-%7q3~l%H|b~d8Lq@KaAi`:)A4!HH ́ntE$tDBGJP( BP(>:z 鉇л;ބo짙X=+i:t{xή[#F&?/0Ӆ >SJtr[K2B5M H4M@4tDZ:NbC@P( B8QQFH h۳g<0z&44c: 1 steHp84 Є=5h~!y=x=.r>JKx.94'^@B&t L#!ea0(2@3tq5ڊ5!4hnN?g͛Kijk0 I$&46dG[N BP( CGH)"'Lh~D:ySpΡ`6S:ޞnZh,N,Ch0<N $]hrɅ}:6l@H8Qj#(%2{:ci݄nD&:zi-9*9`[b"eܚҴ]eҢH,Ep~Bv)$ 2B)$MӬԤBڷFrR&edbs# /pf6~P8< BP( ⨡c@sk]a$*f" $e2¯а~䈢Zl&]!&.{$tx$$ n8wAr߽-/"9]s#f"4[wQ(H#p`"f嚬iIS ʔ K4,JjB"5]D`DJ´R $)5r hlza % =wɌBĺ^nu'7%."QbˋaeL@KS",ZUj&a22EX%LzN8z OQz"9ۣ$t{tbZI!r)a$C#ɐ&I29S$]AN᫾93g#6|H8BP( B1*zp.M9wmD'-9)q$e Ҕ$LIW ळ7NgdoNgI(jM&W>"iԿ&J[Iۀ~Jx[')#74Bs$粚MLCt4abbk;!aCi)mDk'ЈFct{V/!ͯѻ1^0twb?^f>d=$ZnJ LaͲfϫ%Ihn»_err,zzBlٱX,Q? BP( b(ކw|c%t8iLJ$fE⍛&y9L1GO cխA_c\`-/^]N# rZK^#ݔh`16WF@㖘;0ۯ"o@!JN-#Gv'{ChI m;E%xJ֨Dŕ"6qb;]:29b PqDѱ?E4A$L=n`#A8|&9N7aF >:)UbBP( cFGqwlOLEă1 iX;9H"1Ix 4ʋHFzLdyCvG8ruּ/XLnt :@,oXq8XM38!C4.k t3hiwNXk) -r|FB4FH6NIOd^foQ^Œ<.V{{+5B{ T-퍴T&XrvnIΨIt"A HNR`phDd #%I䀓v9UKW޿h ixq`nk-M$քę3g[cڳHԺ_z=3qи ЕP( BP|(q(.E3st5Bsk;p6]3ow?&^4IrMx@'Bh<|HMI/ zڛ%u~#ʴ7odqhlj0 F"zwc̮G0?x )ÈVcgÕpI0ܸt,3DbRңTHCb392L [A-]L3OG?́7bhs +(Є5մVMVހL?B|>E))(׏4u BP( q2^h!kMeܩ$t{ vwơvg C{\֔הOHn˥vIr=WiI ZZ'97ֈC̈sPZ4,PV^MK4jƺuE G.\Z 6kn}B78wdc%ϡC4yLär]:!ʠc1jrIm4 T,BX kDb L OH؉%ɯv"<8t`\n"ڨ]͒%Ų$q*羮n*7|+:ر%EJ"qA$v``>у!@P|>U0fi99++ Ѕ Yc96yk AbVZVfA;h7#;xE>\+K!B1l #X+^KӢ6V.jƹf)"Bg{p0&KQ-?Oya `'™`)8҄66z" [Va20*|ޠTjx^"R~- "3ʕmĜE;1bJCmq1.(";Dj%}8hp bbV)t#Z݆B4VY6#NúˢYQl,3;Dd3X*?NtVV *+ .Љ)m +0*_oMT(1:@vu؅ah2_Wi[p=x3MWBCe+2?Xkځ\ dEV(e [ PapQ(Y/1?5Fk,)ސ*Ӹ5hηsܓ#WpƀKO̡ʢ2ja,uWk 7?U}+G?8Ia!B!JCDn$1D M+XT[?MßG {,(]U$xxznŪ0O`ۅXU(fX6Lu$HKK 01~°Zc\BʯX58l`Y]سȦ]-nm,BManoU\e.vcVIvQl7PVRi9\jbnÖeXRIu'(t_9B!B1lߵ g 6 &Wz*:Ot~J5U.3U:wYS6PmLmq=\CcAV!Lǣ:͸X< F]oa WJ,s Wz ob<2YJ:'hlT"gRߞ@p7DYRC,cp?{Fl5yzc9"!EM4H 0nhE>EGo-bx4~A!B!Hx] cq6E P}Rũp A eRў?FUM5Ro+'B QhT SBn*RVTY6VD"51yMPa1heY=e rh9YӊcLe +ha!i¥6,E0x\d& IDATxJҰ c) 3+%֟&ӌk+-+(o )p02z%W4(ؖ]lP/ 7Q9~4OlhT4 !BQb`:z;wrRHGXWMJYhR(r*+ +hRxB$eم^leҊ\T]r._pPЅg ( mIQh}2q Tx๐hM b?htX{{ [Uid,Hׯ2D;ap,mx8]ݴ K$-GA7hT6Pʓ)[ qr(O6v@kjqar IذG( L*e *7Jɫ%{H$9bB!Bۺs?`[nUH*xV?S_ja\/4exTްQض?ݍ6ˀ2$  [5b0xh´X1t\OQdha5G+b;< њ2\i7'Θoe_mɧY"J.dI?es3M*QW[EUؤܲB!;u*jˑtm5yXvS%b}~FByV4UPsQhV}MyPո0`t9x けW!B!J 4bx$]LM3+t/=~`ĘiXNSm!PZ+KAa;UOc*jRaFҹn t5ӂ-؁!a$%giIC7Fb/{h^`}.KMiM l+tU4Uh6Xc<:6P1c+ok85&=^b#Ch9U˱-iT@HjF]"aEMM03p~~?h Ԥq +,5!qqk+g}55?*h%3v%ڲ(! s]yuEVB!zamg#Tw+ Iw!SYdxʠ" ͮ[?ݍl:P4@> }MAp&vdeQi,UkH3`8Pޢbu믊ָe!\(椱\뺀_;Yi+[(U`PD Ū iq vh/XZ%)'@8h [X k쪨M$l>_Lt׏[96~3FkNvi B!B" 8p8}\{YQQ2[pFMwe$.#J+˱#Xfp#*܈Y bPrPA,eex68 xXlmc(t70e:IX*@8da2d\PVfQVXc8)+Ր~j'\&w.#B! 2m<{8~khlPͬ]2L5{FYAe(]5AKVk9,r7Dy`ouu r$ᄋ"J8`z 3\8V֯Vl TM0܈YU@yV@;EYa\ͻ䵃ϓgKP$$my1<$jkضMej멪#vv wi XcX&5dp²(e0p,Xso P䣬hf8zB!BKEbad4[`ߡ\z9kV%WNG< &BU9zl,°(v"Hg&c;@+5S xL-nca0m*WbEZC(F)ȻxTL&hO:%︤3;42V|XwjZÌ|ea! c G`c UA CML5FiBZaY T/yd7l"|/\B֖B!B̟02{ wb|B%# ~#01շXtun),5v22Y'Iw8#FFO _7`TGF+p]3HG&-E(,/ QYl?-~XexUhT֧kb_{NtwdܫB!@k Oa"K>A_0BTcPBA[gW]QF9W~@'G<^Śۏ^uWCG֠"ʚM;(CIerrѶض](!842 M(!Tm +P ,>~e (B99 Fo$)"?νIUB!B\<^Kx2͡#iil ܼ+PNpRQ^wq-i근ոNl>kUp hZk^ǚ5XDN92)z14VV<0 E ;G5Ir`vd<؇-B!w޺QnZw [k؀vѹ8FK) Q QV c$Cŏ78D_D"ItGheqs3MM TFo!|!h'^c48(J P@ P=7Llgbk1mNgC!B!.AJ gq'-Nz]XL{l&fPxIϧNU!B!Dɓ*B!IxB!BQ$ !B!(y^B!B< B!B!JW!B!%O«B!B'U!B!Dɓ*B!IxB!BQ$ !B!(y^B!B< B!B!JW!B!%O«B!B'U!B!Dɓ*B!IxB!BQ$ !B!(y^B!B< B!B!JW!B!%O«B!B'U!B!Dɓ*B!IxB!BQ{B] q\r%!HxB5v{ d4rqM/. bMkd*@l.^~0{=s/{/ eeӵ'V^rtO@Oa/ _0}3j=ܳ X ]} TW[R` TUF Bn_>LמB|IxB\{_PVុn+lݾeY\UyoK6<5 'Ϲ^OVO\hۖ˵'v^55?gqˍ..K^Oъd?BKW!em-~٧=ND'|mݽ8eY45Գ[O[>-?pD2wƲ,\ {'z`F붳ry^zMv9@2嬻J3u]W|wc,Gk,joqM@(/+cq;tNOvZxP[S=e3ycV6x PJX> us\OCãa+ys/(\OkR4̓M=n^$)*})/{,]¯>Ԕ1]=}CqlZeqzgooy$RE"pU+'_79m[OeN8s475LY杭s?;o:m#1GuuՔGo51b3t:S|OrwfyޔL'rS9s&Ot2:@)1l.G9r8۟uYs>S&:2; O~~:c؅0+Hqc;O:|uǎmys]>YUk7o, 5L]=k+r=wԩ νt@|?\ o\)p\^[?ేI!lGFx7f8G/uNvy, B?䗵N&;Q ORGᖛ˿| 7q  @ld*/_{>3~ywފ㸼}~ҫ6n)WkOx5ÔZ!dO?0O<5U2,N^~ms1r:}qlbdt\>?kh?] mM x@lhJ_rR4t G,.;9O7۵P{1F"a~מ劕x>o?+7pa|0vyXKZ 1Kn!ćTB\R4TyCܺN^.>~{ Z'ケyKUo:۶-͍ |c*wqpPW۱{ߔewOwo?M |' BD+y 嶾\>_|g!,Y42JT*iښjW uF+S~ښj{eeyT?y斺 {=U `wG+*$|_1<:Z|uRu5,jn">tR##_ֺxŶm4xƋe)]8\|?@._us>O7k'-vkY[MݶcO,k^7\W|mb-!t?Y!.^tzj:L1;5 ]<˖dQ3}z{N]ֶ*d!1>>}ZѱNN&yLdZOY}R[nZWs^s?!1OPz-gr>B%+-':y͸o\Е&nR4Å`ج)_sQbYSL]grK#BqnBy5FGgmlpRxVTL)d3B'{]=$  7456}wsW:Ϣ&CK/˯7\y5rñ{Xd;\5)ws5Zr]3.;ҙEN]&|d>X!.*lٶECݩ?9dB3 O>ҳ 1̴3Sg¡e3.YD0,.;PMhB}o|]=hc(i2z;ie1Go]m5_GcC|?̩6GZ+_7ޞe)n;L)pOEbM8ezZC]-=1m{g;jS@# :g R }_d<- IDATUu.zZ(&J&gYtP7;y7fs Ɔ?ߘuZ_x?jË/-SoV6t/YR t]P>ګ?bk_2k+B#B\Av=0jk-[ZkM+Oq|0JKscCsnřޮ=f]bryIUp϶B{c }Vs|I"2Llkݽd5Usc_ObYk#G;,S_W ML[+wMkskk띶B!., B%Ͽ歧-P11^g/3<2@>8Fr};oXո^~8=!q\\כ]\?'/rzR}]MIUa=OOYo&ƆG8Uh yRSsYq6z=-~_ z/yN76riݱ'|*P֙[%˟6xrEjklpGqoիVzp{:ϱ RJ!۰#'8xW/^#ZQRt&8~ˍ"OVTioʔDWYUY2󨪌H&$S~E]^.YUi?v8>Ú\{m3"篿TF+r9C]mYu!FZ^O?Yث&1$/iT4u6q Z3O_jǿCSCP3m M10:g,uBe|=;NSz㺌%H'fsS,k['~bT:C2qg Dh[ 58y!Lf!o X(xL飷P2:e<ٜP}iNJU#cSfYx~\# ztCãxR]ͯ}⩳뺸yeuU%X^oSe\?k/_෸[d4׳m ՓNa ?FuU%KV-5c`0F(fɢ&jN+|4t17fp*U!l6?I|eghB^e̫B!J/_ġ#H&SŋpW!HxB!Dڱ{@ oic_B|IxB!Dz~ ubRoR!.G^BQ+ !ȭK!B!%O«B!B'U!B!Dɓ*B!IxB!BQ$ !B!(y^B!B< B!B!JW!B!%O«B!B'U!B!D \B!!Q`Y-bb$ qػ b+W~!ĥG«B!t5SK0]|?la_qi*Bq-T,tBbB!ć\[?V>|g !.-^B!.\hz1#X08̶~)"݆B!.o5>| gs_Q$ !B\@s _[BW_B\z$ !B\@ WsB ]wvBKW!B_.B?)$BqO?ӷw/tυ>~!ĥGZ^B!>-te?鵙>o.۟δԎ_Qz$ !. Ìxbg]^kMGGeee,Yr9eɒ%uL&CGG###cYմtҳ{ \םu.]J$ӲC,ŋtR,;\mcQ6lp^&͞}Vt:MooY___Ommmɬ+J׹\꽳0Z1  BK;C__g|?^{ullpnwl۶m?~K[[<`p7oL:1<䓴i [laϞ=]]]ٳ6z!l۞>m B|ȹK*3k0+U-sn8c+j?{%tRX\c``V֭[lj'蠧۶illo>-k. ?ǡ7'O|(r{.?lB.n~빮Ç3e˖}v󹼐>_oT+V| )Ĺkuݳݻw/~u8k׮q<ȁ]maqG-4ooo//(++q !><$ !)>fSOc<~m:T|qdpppFioo_d.z!Z[[N9cAYf>^RP?ηjoo/TWWsM7kd2Y`|uuuXz^Bn|H$NƄ(% sO,Wo|ccqd2Doxb,C"m***N ZkR}ضM4=cu]1L&uuuTWW/~E")w> B|H544/|M6q!Z[[yGO{bp]bsP%x7~ppv*VZE}}=曤i6mħ>)_ꪫ29.Ri!xǑ#G@P 0zǦMذaü?`Ng\.d,1}VZE6z˖-SYYɢEhnng +PUUEUUV󍗋(-&۴i]]]|󟧭 c \PJa6mc8_yW7(=0STIl|r-sZnVn9y:::Ϸm_Ϻufb+JTǝpĉ7&LNYqcx9vMMM|3atto|_ghhߝ/~AZZZ_Ny5/WWB!>OO~—eo޼y3g5kذa### V;w瞛X!>|$ q)՜ow+bJpxb(dX,6%\ /M7ߖڅc=J(Gzqv܉Rn>?O[[_(// ˱m۶bxٖ<Dz,;޽{y饗֚~攛d{|r"vs@O'Pi;>>~n; /Kqe{="O<ļǺn޼5k׮l1˝ٻCY'%"((UmmV[moնZb["@HdɌ3L2<$s̙̜9n튍Ujj&O xJ[fdD 0@\oڴIǏ70S㤶5ǡFcƏ/˥j >\Rӹ>_Cាک獏o5fSnngjݮ7|S IvvV?BIҴixD[mm |͹exxkWpl"-vTUOm69͟?_={ =4w4;0uA>ƍ'AI&yJܥ?W[?yw_7siD[;qℤ(UWWN[PPSf)22^;0loힷռvH%iɒ%KJJSr:TmmƄwws@p"_sH466z:䓚薚>Æ ێ+J*=^{M(OEe>裀y(~%U૏ |ŹYw`((**tQ2T;w s(++g}&I5jOҖ-[$⭣, &)pRECB;}xWUlo=%gsq,n%&&tꡇҝwީ/znC=$$ؿկo^O?-IZhziSoZNk(.IMՓgϞ{Wx`_^'Oݻ5tPc9991hY3ǬYZu՝cǎAIIIAWTTx~ <էΈ!33S/qڵkv%D5ydϏ>x {z$5ov]r\_ǎ'xBw^}ᇒD=3߹瞫{L<9󟒚~qCm6ڳg8SaG&`.K{b՗%_}}kl^[oog/:=YY^}U566*<<\TDDZtrss%5"Ţk tR9rD111RBB222dZbEFFjܹJ߯+W*,,Lַsnn~m >\sNP[oZxqPGնm=U)K/2M#ƍtdsqz;7ܹg8βZvǏWvvRRRry$b(;;[v]=zPdd,jjjTSS#˥HEEEr@'ͦZg4""3oII$3..Nvg^ݮرcJJJRttl6,g=rG=U dhˡtrss3byPNIi󿳍.i75OjCM+p84c }ǪѮ]dX|kXXΝ7jǎ*//T3VFFF&AiժU3fP[P)*ܩD:9v오}ۑjǁddd/֊+w^ݻWRSOӦMxݽ켼VmݜNqW }|!4ժCjرmvР r^;ej%%%no|m>J^3L%n *))QCC yeeeTbb%C Ѵi$5]$+))C˪ŋjzXB'NuV\\|%%%T3}]=Gyygɸ8G]]JKKUQQppՔj䵣aem]rt:ۜq:oo-׷lv;sᶶe'M]J^B+ t@Pt0)xTsHLLtpr&-X,Õ30S(᪥ ws{~(L-{{?U8^%Ԏ:ҹJ u8^kNM~g+8M81ZWW3T~ݯj#Yzz"""Co sjP04~g+1t:&kry-T BׅrynY2C]^{ox[[y~cY(yFJ:+9%m"väP{m=CxFuEtjlo-pf!t hZ,p+@76yݶ^?6=W;ݯ^NS/`> tj ULζ!Q[jMV6myݤ+ۈvu8P{ 6i)LWn`UWWI\{C=#Y4W tСӽ v$''JHHX)e jPҩ^SzNSuuu*))Qfffe08t IIIUII^4 SzzO89ӽ@e^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xWO ߼G{@+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW8%:omq $itwhكO-ߓF}|GCos;o[۾[ݙsiLߖ30~ww4x`^ib?+U+5YYzOf*u4;GQUUu˲ZԿNWZjjj{vڧҲnXu%3YSMB~u" VNgDkؑJ?12 !T[W{v_!UV^cy~20KÆRtt m?6HRTd\ڲ}2t֠`!NGDk~;'>*.6Z[Kf)t6k __.kTӧNREef*&:JW̽P5uz7vƠ+UxnXcc~=ѕ"#dZͯ_]ӣy&w]rEFFLEzῨ3b 5ux\.Y,I+K;KW]bE5)Z=qt>ӯY0W,yW-(퐤$|+yoA/+57{B\.쑨}vn^7tdKC߾.6oןxѩgtږ[^eԏbb=z'TT\Ҳ.]}UnZc!vH]Z-ͣ=l6]v ݼx f]vX>|)<,L՘g\:w:]-[V%}f*gr{gRnI]?xX11Ѻn8nY0WO>g9k|V[͋sתwf/-2 ;kP +3=MK@\e>t{ KivK*ŢwǍ*)-?oy'$ejڹjg~[8RlLLgN{2uh 5):>.]髴-{M{Оtı7ߨ_A/k19jll5'kZtelZӈaCǎer6mݡ=\jM_-ݮ'yQ|XavmؼsqRމ%\RS{,G\l6 WlLqSaQۺPRR*+U^Qp=l6zHT]]}]\\dFsX,qW^UMm$g^Cc͜~Ox3kΟ:}j.~/,*я~zmݱG|3}55ڼmW55~{QlL - SjJO&uKԴ͹PՅ]Fz&Fl6F=6MRUTR|R`DGG)1!ͦ ' mޠؘEF+cfWJ**.>qk/,bcںn VUҒ%tg_z0]qWQQkZbc#)QeZŢ45:u2P U(瀎wNRbl6 e&Oؘh]Ϲ|O5h@?3iT^) ꕖ^)ݺuCx"}zs'/O=j]9o͹P%e cTXTkn\ޔcԧwv=\2zP-^4_=%IuZj^>7'k3$7hϾz%:YP(I5wt=>uwh@>ݯj}4̳zWtEt?mںgiUWj nCcׯ=_ ^UuYRoߺgs{3rujlljLؿzHԺ[|.O,PN :X1Ql>Q -[FIë$崦Y걮vW)66Ft:őcz[游}s=\VkS_o{ߟyQE%m#.Vl]|4x<ϹH.KDghǣ"#oׄ#=GssHvғ=ˀq`-7[ϹgM̿ʪO0+I-\3MI~9};nԒ>[﯐$M?J^y|$S1zP7UiYbc9bfTI6un*UTTV)?PQQs7B+664jO_PM;wdsʵ>d~m⹿@Ǝ&ͦ{.>zLiJJJ׽i^bhjll}B'9;Nӻv*%>XFGs4u8 :X?/~f)\\Ӆ;KWiQ;Z9)=5aߴM{j _Wͦ{HOgjמ#IfLr:t8[zE㴔dM?Jee6ޓ8Q;**.fՔc4aH^2dZem=7nӞ}+1/lVHXM7$gq e_HR>ՒJG\狎Yُ̌4m޶KwUccfz:e-iHӆ۵c>IRRR'N0FߴHU;t"P UU5±i`zڳ"#4 Nϭ;mάf\?,SYyg<.мKfj눳d~a6=U[[>|w@[̛$G2 ݄Ed{6D5O +󦭻tǷo~J/gbxdZU{,.) d#&&Z,+ө| :yb]5nS]8VO7$%k.Wuu2l uCme;[QW>V|j4)?f֭ost&O>ۤ۾HL cG[#.FNSezoGn>ϹiZzm֌HOժ{ԏ~p ПtHOT}}3cY[&g_DƏ*N7J}3}9NϏLnoG[c~E+!ޡ>}zϿiPI(ю싒2*qSWWLԖ^J.UMm~*l4 ы}~js?n=w$DGKfR?}٣OT\\K/p_9VYUk|nB}w;fC m(;EYqId(% kg;*G\yysWV<ͻtG͞y͹Pԫ^aT>WjgڈaCРU^C2ccb<>L3kRs'Ӗs\u69s~UTVi@Vxz_/#ǚےF UfF/[WUҎnoG;G-ө?\3I[:O}b>7請&)I;9 x|mtU\R^0vD~sRtT֮;4e6l&G\6ntDX,ED{{Wn]ZѧZfZKܼvMu4a(mn=aH/\q\lFO![ǹ^cu+2"B7mӂfkВ>ЁCGrWo +DEOkԈLO A[!{y1q#//Q\l֚O7Ʊ-_P۱54_wݢ[xXfJMSϽҪ$6];Z=s'\Gw|o'NjTTq;fttOiUh t%3ԯO~qsY!VLt:o94At-\0W9溅oQ]]\ ӌi(*-GolsAQstSO=wxw*-_ϴ@f@j(z}Ҍhty=<ѿ)vչ5-sΦmhguQAa&^}Sd(5 i[IiҒe|ګs~9GWLOLWݮ_ZѧZ|uEDx"P]}͙WtUtjg^MM%[n]{ iڼqw@1rؐVlnxjUh1lwA-6Cvi+w.wϟZhЋ5{SsڲدcSuu ;XWjj/|ˆCJ' Էwvv aavv^+*ɺ~dI/y3w戮teKI;'c]QY7[+>7S'ieg^ i;n=JsreӞ} j!9,9N\$e*3#M=Ȓ$ ܟ67j1V{@~zT6]\/;| ?{6n١}P??=}@ߢ믹BQhsy[ŋvU|*+Ѐ>>h-57HR~}24teWQC|&jS5nwhvF<[sfMٯOf6fWMMҼiO^:elݪm' g! Lq+!ޡO>t+n_+*SNUOs###$Iu=k/]EQqmqQCCv;ymG=m/3S;t<m$yzt os+::*e*u7?0wԼ igIG>SeӶX,bE]̽Pee;gz$c][[7mj#)uk{\9pJ5u]sk֌2q6mfW~}њ5Y~$566c63zzzPV|߭nMwFkZ:e{Y+>Toڪǎ+:*RePJr== IDATvЏf?Edi9~Bq1JM1bۺkS\ʪjM?Jg -viW/nߺ* W9'S3ϟ"˥W^gFSX^y$?~ة%ө'}Y?F]`b9jm4e}s>%;T]S֘gbty[=iN}qi[~&lEEEj֌TU]VZVqk4f3lkjNs[M҂fw1RۻVi: r8s>UUU+!!^=Ғύhj3rX'Fuuj!ץW)5)=#QgFϏA)={hEРVb݆-p۲m[8OϽHuuںci;vׅW\=s\6U)=ߡv{M?Z}{gh'UYY! {՚2i;gzHҦ;UYUsPIPũU:yf͘^iھsS;vj}wi՚tD"##)A/VjS"EGEy: [{-wNMM^xM{?ձ< ӕUlwI҇|{ +zw M0Z ڽ&~}2b6Cf|GUkvFxbxUmؼM\yfLNvѣ?t'+y'uhUj҄Ѻi>SlL vFUWhZ`\-Hjseo.-(,֟?V^ysf*:**`P=Ľ4#ٺt^~쑨Fzz|6- )WSils[ފK=uzu W뺅K Jǿ=0'g kޜ [=^ZV M%Wuk5jY'Oɫ1u#5}^ 744Y7~JM7JS&$Ͻ3##S xyXncuP\;AS&{Nۼu^ۺ~𨨨[^C>۾H))--+WWS(N=ǵpM2^C 3ʾ8U?zL-4NSE% R05Zte;2[A8e1Ak>a;n`s֭ߢ p^ ᪵: =x5ZyOO{ޠs')4s/~\}CNg.n h4o KV7ټu5=ϰŷs_P{ED+3=MUU;Yp={$*gɂ=]&%&(:*Ry'^Hߞն{CUmGGGss?>թ?<3uK:Ox~1UA*pbQr$K{\ Fݮ^RT\\f5뎊WxXϲMYJLWLtjjkUZV-maaJIzUTVv{}##"T^QraO:cEi u1QJC~{X(1!^ **.RG֫|EEF*%{KX,JڡDG)-5E9~{gNLh6TzSR$g_y;ljkMu_ Lvں.[8qQKTdA fmjhhTyr:dK_P$ө]}%Z]6BRwtŢaC5{ԴJLOC^tںV5:A&;#ױܼ._`y(#>rBj[ʪjU \!c]^Q!m}S]S#-0JMpիOuk'wݮԔrttxlז/ZOq?~WIbBjjjmk3kzÏڭ盕+U%eڰegH?^YJLp(,̮¢>rS?t,Cxڽ`P!qGt:>zLۨj;0+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+x nV7ټu5=0u_P{@AI;%5Jr6wkk6nZ<]^[pJ+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xW^#Gx 0`<+xWoZWnߜߚ@\U*ɲ{O{-hZ*ɶ잗oJ+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y+W'^(OPx< @y}wm_٬|;Uه؏+a<ّ:VC OGk-?ygEkv>]WNzZ^8{?~G}UYգv[4♳UpĹX3zDoۿ{9zlw}n?~{پn?o۟?ߛ'e'|kG?4ol[3c폩=#A!{o1+;F al1چ6wDm9ޚUf,ѭM~}{{gو+|h]y^͏ߞ]p<ܺǴq.uę69gzܣO^go[>-~Μ~8[7^;{lCY׼"vه?GqgϽxVGuv-uc2콭_Q7mO?{/+p] ە88ǽ2Gu/J<~gf} l->ζ>m[v& e^̎^#~fC̯ʉi5kDH=jg]lh쟭G|Om6~UNį ;<2`gQ;e{Ul,6x#v6PozqHm#w |}+`U3qlG6+-c,V->kv?M}lڣPۙ{kۉQD+1G{Z1X?w"^H~ۇӇf_߃>VJ{zvnm< Ϊf,VM;7ڿ}\ %mpj }!<G>5ϼp?peQF|ymoԟ>GSח^6Fkܿh===Ϩccm0,VӃ刟3ֽ݆oovr11-.κd>zGcU9y?͌~m6`݄x#^, k[dǼ7;-G6fv虙tiPag3n]oI(^[^ˑxmڰNtv{7v>2~g^l{;quX;޸>^#>ƪx2 myQĶ}">jqmm IDAT߇9YӽͼMCgq;:G~T>Vv9uu+Ylg3Y΂ k=8gÏzumG7֞ƻn[4׮ocM۶lfuv-l$c>"g:un4~vkf{ﳸ۽7 mZ6\G_op4D䳯>\^,ZQH]]}knj浍jhuه݇4f[G׻y5yݖWvzv61="j;\;;:[ȎfgGE}>ӖvY,lֈj;Qnϱ7/-_h6r[xf^Xbvt7s/Sߗy`uFvߒk\ͷyl]k6˚l5cBf(g8yϜ50 w3nؔj$cE?"]d5,f-G3htMʬ둱'^;QwF3֞&ͮOg^y^ YfؙCt^5l6ol7Vjc?}x 1`"~¶ڟ.<5Ek^un;gڮG֍ynܴw97mȎi]]-nF(ZG׸AzæU"jbq69 QF>OlF{7͚"ymG[Ͷێk]e5 Fї3fG#fadw6{<}٣n4>XGtm]ŽfZHֳӆ2fYx=ݰY f=^Q_3gWG3fm}{:krYՕS#8A e%NLF:ӺfYSG3{+Hݴ)ֵSįa>}Pn NlL~`xM;%we46veǖE=xӦk_[mlvkptq6ݘ)ccʂne6so85b =MYScyjܞ{4`Sk[x|^V 3;ux[^];8vg1z?K<{^~ހfcu/T۟Wqݛ#SnWN۝ı|6\c2~INxSg{׏f}H_#{u2k/^gbq4Kl f?/gNhl->6-٧u݌nm?Ȭqtu~,Bg͸fauc5"}JٍG]:m]=]l+y~+쮜Jm_71 ӳaSYWf`{? Gh:5٬k,g+?w:#3гQ=zDxmx,ըhvlfV+Pw6t4MF g5 ҕX|=Z,^F?fkuoS#.׈lmk̂ulqlը~l}=mxtz}~?u>.g2 l̼1 Εh]ym<2^X]?b=`El6޾^fxוukIWbvTY=~ӆ#G;}]_={ӃWN m3 D6<۶23]]1xQt^׾ ~Yy1ٛ]ݾ̎i8vUF<'^ڼ0v${q^J\پˮ^z{*l{[qF^&vgOpql^h}&<`ٳocAzzx$`#ܜIgl߫uvlO ׈~f{#+_Q󞬷i_>_ ӕ?r6ܮ˯YA\\GѺ)=?ytU}*X[us0b#%q;IJ:=y_ˣW o;BS]<tIwaԓ^x-O-V{}f:#l+/pW{W{+W'^(OPx(Ƴ}IENDB`circus-0.12.1/docs/source/for-ops/web-watchers.png000066400000000000000000006551141256046442300220230ustar00rootroot00000000000000PNG  IHDRb iCCPICC ProfileHPTY{ MHɱ4Mi$fQDD@pȂAE 2(&Ƞ*ک=_:sλ@>` |g0  skhvc8q$ٜ‘V"§US@9"~T,# "1\0ˑ|d.&aDOf2?-萄sٱ\HDɳ9$&&r6:o"M&3Z56?-1A edSgkFzb,p TgMebA ,rZ$oQ<7rH?%lbCqu[`~(>%-m1e1ɟe9 9&wpRM 1rD Q|E?&_/$d3]E  S`Lq*'#u6y$^&?6:&eLML}]H+ )ЌAܢOb}82P j@=8Nv.@Fk0 >ipB C  "h lh;AePB>hz C;+ɰ4k`:{ÁZ8Nx7\ Wp|߃kx P$,Je\P0TڄCQ-NT/J@}AcT4 mE{,t2z]G/ѓ F001јtL.S9|bXmn`a[Av qv8_5.nFq$ s%FymK4AI!؄LB!(p0J&Jv@bq+BB|B|O"H֤UXR)8i,E#nrBhQ)aTnJUH!,V.&v[8A\SI|xx) -  &rCSTII_DF>1)[*GFEUPYԣ+Qi4C:N:_LLL9,JVK! [({R%JKpZҲOrK8ryrrʷ?U@+)RHW8pEabRۥyKO.}+)+nPQWRRVP)T4,\|^y\bRrAMDK.&UU=UUjjAjZ՞Q=*+454i414{5?iikhjӖfhgi7i?ѡ8$Tuu҃,bn 0\j!Caaᰑ6v74-ۻwc ƏMLLt33e5m60{ko1?ljbEwK+Ke帕UU]G/_X;[obcijs/[CxF۱9ˏ.ScU iGLjlZǗNNqNNog?ظltvEzI=sWsvor9Pb I/+^e}|>+^+xRs%we/e}**U^gP4| t, |$ nR"\l7BBcC;paaaSk_3n~ڌ}%;^|=sLDHDc7/9Ɉd^q2.(j,.z_xCLIDKlY8ϸʸOu3 ! Ĉij\)n| !B!B!B!daОB!B!B!BxOԻzp cyB!B!B!Bٹ%C`= B!B!B!xvt0zP A6?P B!B!B!BP:~g t`pC!B!B!B!t+(=tg LyB!B!B!ByLcv 8B!B!B!B!Q.e/7_j? !B!B!B!dx).7=Ԡ}yy C m4B!B!B!҃u@= 1?qvuЩ׀ [ rB!B!B!Sny o @{'B!B!B!|Pl7 H@ 'vD0 ~oB!B!B!B('fZN[fތz83:=+w\~ߡ7K?7ٯ7~B!B!B!B)PN0z o}onj+w@}L?Iq3|[G!B!B!B!dSڗ r< 0|E7 s}X/7_r+_}7}!B!B!B!dWbo7Kmy >+l{̺r/`Hm'B!B!B!@߶/ @^SK_r ;~o{B!B!B!Ag@W>f[Arq T5\a s o[go7]l!B!B!B!N@^NO 5 4]n?.8_*_z3ݸJ{0!B!B!B!dW&.`ml3o#v\ f\욊Vvd ߤ7 /7_X^▋O!B!B!B!L_NT`?nޛ7;,po2L?)K-I5?"間M'B!B!B!|P)lo7; @<{K'.rgC^H/}9oo/}c瞋&(B!B!B!B!; L?dɒgo./'=nC ׁLmH_+N:׿gLq# 6k m}?֗oY|PWr3mYz^y'3}צ`^ն5~IKL?_!ʹFR??????c/u;ի9oXjZ~q|Z0vt n-˖lضq?я:XtE RH%h*sJz٬fÒ_,P2ЍHŝ#w\q0mQA8NݹHKOy_cܞ>Kv R1qf#1ӊk`nG*s fC2;`YVK)a1]fv.:JGquPCFOOOOOOOOOOOOO#< O#ۿ͘UbbRqfYLFkRoƊ62֛q} ` 6< B+]\/xQxYgvfuՍ>"KcJUEͼbe']r\k+K]NOߛO/_uuY3ӿ^OeyĉGXB@7"Qzs`e2@W#4l6[1 }ܲ>^Y~u IJ,/=y_ 0qqGaqu1 7ƵOgadHZ:?: Ͼ^_^|Lso{qU~@~zَYo,[r>[/R!,}V? hc![6cf}Ї茳~s#llڕ/oߎ ='^ Ao;[ǜpis#O*l{WУ`Ś1~QcXlw1s<|30mّFb62i\t:f1i6,)"H4'fYxkvfGIW\2+b217n])q6Ӑ27n,3?Xz5ZZZ0f??${1,\;k⥗^ƍQ[[=ӦM{´g}6:}]o&,BEEƌ3gp]v̙38OOO|eFO֝ɓFo gBEI.Ab5e+f2֗ ܛK~oݴu6?LYtFr(h~oX_p3,4QƇ~Xm#k2|`!t,>uw?8V^ŋgg'}]L4im|)W^wߍC=ׯǪU_|L&J???FOO{K-cɜ&q2-&yXeG+,Pa sg`=[V,8$|ttgGxXt_Qt\l>n&ܾ=$;Xc 2+yx'f80u}Д{J0} }z,Kxq2wiaz@|Ǥu- )w7|3yyttt/_{W]uo0?T 뢫k}Yu]?HXDҗD"|7x#QWWݷKO??}?]ο ۿ}-V~ )hoY$E7"z{\2f@Vb R7Wrysu+7ۃ|G;mUb+/"8HWB5fLCmu~ɚ:8J@A 1{ApFK{;a#QSW5@ue=$ ϢusH\اoik8Mwx8yYC;1i9zɓ~JpE/Ǖ]Ŗ-[¼-[ƿbʔ)?[/Ƽy~}]- 777㷿-z466ba0oڴ -B.+ǒ%KpbΜ9q5ꫯFCC,VZç?ilڴ ?pcʔ)8묳p=k8`ضoƍíފ/o~eEʓnOOOOOG0 o`|*bk;/XEЏ/ۗ#uշWt**kBEulv!:ީ" IH'PِFE}-upR)@HT Tl В8LՕUJW"T H!I6ʮK䚥eoyH7j4 Ow0(uo9G( [ XsI)_k$QI݁KS43?īٱ&????濫 m)qxbL6 |7oos=ӧOG{{;4ā8<@"@xGpQG.TWWeҥ?>fϞzXd yp , t_W8&Lo7nɓ8N$mGuu5."~폿H&8Sf1sL̟??88umMܵf7^z)lFEE=PydYXnϟ 8=\̝;cڴixqc˖-xqI'a͚5壏Ljjjկ~7tyuY7o^cb֭%777#D&yL2---a~l6<:MS[[^ohkkÔ)S<˗/bqgy&Ǝqơ3{?<eL4),S۶1bd2v?$---mC_3f@SSCllOOOOOL[{|?D/?l-mnl30طz3omT.aCvضQG#Ѐp˂׺9'֦Ns@ O8QCK;+pR* bnƶ3 LS#$\ڣnҐ+_CI#tG0W׫uLT+y7????濾 xꩧ0eʔ#ykllmw)3Y󺮋nvo|ڴid?W_>Or lj؈ 6D%f{}7o~ߡ#F@.8} h8P߽rzlƛ}c9.f[-7_C 䗺r1q} e3oT<ݸGNš'3+Wao*b@u-fq Fѣ;]jFa9'col1MnwE{w"*GeY9ՕⅧV`ω3f wwFnTdjDiҠ?WdnRuп!Kg;/IC+IaC!ljtLiJe>Qd^[1r]ڇ(ο^'ڵPJS.R^z?8CJpw⡇ˆ#f1qDvi,+Ys,xOS\{B*WU$ tI¿)zmjj"L²,|T*+?qc¿G#<A裏FKK  㳟lЧrDmh"\׾㨣ߏ 3g΄NHԧ>_Wꪫ>s{G9rq:06qA-SZɝur|Gaיy쯟pbŊ;uCJu2݅_Wu&@;u&̚duMI:&R1$vl^ ζu0j~|8ɚHCkT_x3qdrXU&ZZ0rxL3{OT-,kk0\i]_^/o˲Ʃe䷜WD}\'-isMY\۟eY_W0Lcuŕ=?????N{|>.xѣGc۶m 1"r.qPWWl6 ˲Nm6oYKlLdhkkC]]2 :;;whjj˜1c8,aYjkkL&ֆ#G#{thnn@cc#ZZZ|CC,|G>G*Buu5ZZZ"4jkk܌zr9tvv5jn⿪ hii_ʸUUUa^ ˡ۶m ">#(rܠ'r[VtYvn\rݾ__p@q OH[dgٳ?  {X.ԇ [s9B.VZn,osAxe }qs+Vȕc^T0y^ ReYAaDIyt,ˊt.TXqmH"ɹd9QH:r`66A: eݡiXqr_(Nk0m:Sϙ3lDłv }~}eD78o-c]T.ߏDWl"@o78H'!Ia6#[*j#O}|ӝ>Y/n0mΏQ~z t$ϾK_;Gݠ-:Z]V}qeq;Pw7 }AWz01:>cZ~˲e" FY;^yłEoޛ/;%~˧3̩RuՍ蝚OGX1<+y^l6+v9^7|>ɗp$ }n"uCrn]V r2η\ttͲOqGwSnwIQ3xliƥ_֕ b|3ou_*X7ݾ^7W^Ru~a%z+<9Sf G*~DҔ 8Nxy H*eYaoo}}҈uC7_:I9H֝#N73Ӌ`g ?2zay???|??48?w;~۾XgNEGC̲, K1S뗋}ŽyS3F" @l} +4 y^PsFZ1f6vyE҉WΩ\KS\Go]fO1J7#H~̺_5"#1f+s?}]t^ <ڥ.CӻͶ&]w:F:Ƅ L&rڐd~N ^\Dt555HHR}Jڧ趫Ml|>L7,o=} XamʹeuZ;m]Jnr̛ -9Yd q"o2PflYʇZߒ^Pd/7q-c> LׯG}}=NRObR[u`|໹ΎYs}[s]lT ذapG#χa6$`۶m?~<1k,yTTTJCz' d@qXtƍ  2sE?l$)0o (נ]:<GȘ>@a =͏lw*NL,M;2e뺨2\I[6>x^GosR뗚Z fYA\^ޚ/]cu@ߊgSO=iu]XÊGA;̙3[oaɨ5=G=zPу&5k>q#> ҵɟj_5)L+#~N_uM,'Hr]̖`ްo)~7/:}) ¿x/X"Dׁz %K Nz ;j.){6]9dBѥ#kںtfzps9Xn̙t: qJAyo<ˡLUUUzD"|>2 9P&OɌ2ʠ*Ǻ/|Lbr!/yYt<@,enO#na E6 V4zK~bۊŗA뇅 8_Aɲao;˅}r9iHx~m ~18/x7q50aMޑfns\8g8dATE?gYVdx\|T* +<#H%xXV r]qQ2 C|ˍ<7H$"y>oDd cMotx]|>}lMO.8ڿёzMr ƷgEv3@o.CKk %//Xs['"|lA5G[1|'\<777mڄzTVVL!a@_q=P \@d2@zIC%M9_,ˊ ^W$HISΥVnD<|<=@'I69[!$A$č$>Ͻ2,Y;F:zt׃+D7dAG}!!Le dsU'1<ߪ'yZO?I(<'}?|Nߘ72xJYJ۔@8K6=8voNKnJo_F{-׿Sn???????????Ƃe.[b׽ȷt/sR[.}"|i$/Xҍx}c|^> 7( o̢G:9{ܻ /_D:FOO*++/7 Z,_yޚ<@!:[os̐~?O}x' 1cpR~ ޿O#,K@d@g}" rz0Au]TTT 4z0׃ 2 $ۀ޿xo=x.e O.d8te`} $rJd_]2`q捏%JHw#} 7: SߘuȍɈ???????????PfތKXj}f@EW@ F~9~// b~'AlXycs)s{qxݨ?Ɣ)S([OGmm-.]yaƍxgJpEGMMMz/ԧ>^x?q' /c! p7xɇlꪫsᓟ$RԠ<|_.B?I#̛ߔ)fd:epN# X*x^aJ=qW4t p͛Y',R#7 Z<āTkke6l8|?Ç_[ɟ.[}c"OAyŎby9\فbȹqAbqOVX,i€̏}<?aOš\zn7~6oկ~6wy1cЀO<T /q1`۶m< ,-|Sg?wL:O=Z[[`p }> ,X^z ')S ,[ O?4 |3/Kr! /} .2wo~X38l7xcz$׮]{bҤIhnnƵ^iӦN}vaap ;xb^?;g}6?p>2{V \;͌e޸kNaۅ'N Á-K. ۅNOr>xe + b`)4@2Pʠ(}Λ8LT*L[ՃΗNKkzopAY^yRPHto9^ާ%ok7zkM&o)/S+O&)q IDATxccu9f0"S ŭ+~@ W {1bO>;ԓ KS)>R)WۆSbD 8~g۵p3'lv5H7NWxn |ghhii `Ip}_|quGOlŋQYY~^{-K/??bݍ*XJ1"g7n\ş'̝;r`޼y s qOS̘1_N; A`xqףϛr .<ø뮻~zXضmn6_ֿFq$uWFmu{:"'O>@deer2uuu!LFs=t7$ ޛ \.r!V_ΡF\s\XV2uʶmrZ] 4=3ț79]jG\SI:ӿM<(>eKHfOOOOOOOOOOO?1~\0?0~m㊥7h3?{R")R4#*ml.gLണ:1rU!p)v^9/v%O?Xd .袢AߩT lf.N0r. ^ɍ죯Eґɗ!} l6̣S۶ݧ |ėy-7r}܄)8I_"G#7{COOOOOOOOOOO=r1gs-fK qgky\'*"Jp]===a }·n U^|>l׿5|Ir9[sF# {…[n6mB[[Z[[NQUU_|@#hooqEn@6իC ~;҂z˗/G|>xmmmxG0rH̜939/iXjUXAK/+W / _d[>GWWlXbѢE8餓fo#eYXf >ѣc;=SeEIe}y([nEss3s˖-hkk֭[.$ ){ qO@l6|e?r\C Iok2bC.3 ЋNK- t$ $mGҗt=/:g>.}%%X"l~u!u oGpI8#uH5sNw VX٤`;2Q@ +ƒF*@*TV8ш#}l޼D oh ~r_Vbk>xmLHZ {/~'8?TQQ|>*yTTT A $d2y, twQYY;::0rHd2x^IjtwwR)(@˱&)r9<voz([1|P7@$aG"@7:P}|)/u],[ \r l_l~@ܿ}v ~ۘ4iFq†;;>Fwq 0׵ؙ3>jpxWqI'<Rpd@”:z6zGp|ҥKq.e98MST}R\ `ߣ OiH0_*G)}H+ g3aۅShٕLٙA$o=8pO“}mG|>+9t29p@rHR72y/1²0ϲ]k<8e0u'<\< Fnvt^ur#lFEEE8K>d uT:fAnӗAysk7@A%-s*#˲kH/O rGKqbs8RޡDl _t0ǔs\93nJhY'@mihwcm`֭ ر##ƍ@Mee%lLYcY@SrU-;% d2Td, H&HR}?hd`TeEFi|ߏCo2dO(@, 2 0fYAHYI_,NC<ȃrr~x`v OAI#7T* xvɣ9'Hn͚|???????????GX/@ *Z?;sj}Xo,ZAbM?oB*mww7^}Uva?>G O?4L2̓ȗPU?(L '@8Ȁp _b<ԩSQUUyM_|2zyNF} e)H8%/<$zPk'}ߏ @x9#ʍ#_AJ~\05PmEX}.Q7 2up*GOOOOOOOOOOOCg^ `JOoNoNoNoN/3>J|#B(]j{Hڿߧ*,Xesh֌+Vq|C?r!F&Afdz<B`[,Xp.ܼ}ܼ ۲ܼ {>?@>=ͻ`a)Xz Z[ZlFIv>GGw<؅"@/pz rc;-0?R>r| ۞'۲{> m '6HFYQ!tdk0>H8 )$DX]ڑ H9N|>PbSqZ%X5 .NAaY{wcaٲeغu+tێ@HHHHHHHHHHHHHHHHHHHHHHHHHHHHW$|8l:qG /2(HcgWM?8hOW,/l/__ X_ ~6_]Hщݟ)Wu.8)ܕ/Pɲ Ǝjnnѣ 2MMM&??niq:7qBpzN Ξ4B ~[ ~/W%__ ~/{˞4|O_ pTXpc?Mp䟮h7V4ٳ}aX())K/h4ү_?jkkq8p8СC{$?k?x_ K }/{+./Ÿ?x_ K }wğ.G~oCo>@e-=@ff&\uU466|r1x`bLOEqK ~//___ o20#/࿰{*mP,UE ߺu+;w䦛nb֬YvzȲ9EѾ{^n7)))vmtMܹ[FO:xXåOW_GO ~S ~c?] ~)?/O/Ft%W?P [$z# ?R~_𫟽!ͬoڴ ߏdrŝo߾{Z #)2 0x`z}P44~}}=UUU8qs~(11}ΦHnjjjyA5X }]$ ٌf#33lFc\i?ju+uXt&mUlq.XCwYR4PI0:.w]~Mon;aJ'batttpwLSIN&##)SX?m?~<ŤĜf,jiiì]2B^pz<9Bee%}%??~أ6o}s>w#??ۍnGRWWǀ(((:Z׳}vƏOQQ)))Zra֬YE]DfffDzvqՁҏ-gI"֕ղTmONNqommСCZY'j3{'wݛ?R6<Ⱥu>|x7g~njP/b_)A1%hqDy0]#!^9sxzeo^{MqTPڰZq8bI/Z: ~/_ ~/_ E!!!rV^ґjJf $1|cUUU?Ç(JfጤDP={DMo`˖-$%%1}t^G!IRks>7a_.($''Eii) |Wlذ/u_]]PbUp:ÕIuu5ƍ;cu`TW*OvYZ,e,v :+ܻ ‘ܿ?7x#z۫6znF>c.처*-F]‘Rj}xΣw o߾dddtR:Ͷp=Ǿ:GN_/v#{xnŔ4ZaE`0Dy{^/_ ~/_ s͆,4774kytU/6؊0LŘ<Ym w Aw*k۱Zpl2ڐV+v=&^/Çc65'>;g#ԫ"|a뎀kvn.?>{wҥK)S0l0-=zƎKBBdРA'uo[{챘Z焳gƍ+9rNGWWW̼ٱI|$@ EAV@Hr^}3!!3gr1XFF?ILL6gBȏEovustuuoc6e9lẌ́ xw2ev?? v3zܧS֒9#)>X.<,Ggԩ̪Ji-[Fss3v/]w!Ǜ~0_ rrleRSSCTVRSSzΝOooذb~U<3222-{XV{1(++%%%%bXp呛EFFX,$I"++pݑlW:*s 0}hzvHXBS͌K:6) (ҩ(PO̗YQPΉ;رc)//g͚5 2erssc˅ ‘2:~81ƯҦ)--%%%E{Gs˲LAAw領4B͒8ւvq9x.<.'rPEƟ=if5U`seȐ!> }ڵ>|Yfo0x`]M7\^Ep8÷mF#fɒ%ӇFnrrr6o̎;04773uTCƽU{VXANN!+3ſuVNӽ*)?aÆ \~g?V_ ~/_ ~eF*++0`>//"˲a?~[t6?KB8tԧIvs3l |!ɆNR !##H|8'm$~ǯ!{1NDO~d45o0L}Qa7_:c$%%QZZʥ^ ^4;vQ֊+شinꫯ֦sc֭ddddرC4DYYW_}u Œ%K1c]]]Z,˴ӟIXj{jr n Duu5s$iWUUQYY_,_…Ȳ-Œ37x#y`$I:dddЧOo")رc$??lBqq1555s=GBB:oddTvUU-eϏsJ?^epUI>۱ᧅ d$?7yi)H ^$~f3q:QM/+9QO~¿o.EQί~mx~WҲmK[M{777q&S뺯zhkkKǒs9"eVi pܹs4h=vԤ9nV6jQEuGS[[`0/ArIx%# fiJ.O>CӥkVT]]|@}}=3f^`s7ҥKy׸1!sc444p}tscŌ=իZZ:R͛7v`t/"{=RRRڀ/f0=7ִ_UUĉ0aѣ!ӓ$I5%77#׮]7adddأ߿?$.0sL^xoɒ%<Sl6<ׯ'!!G}+,ogg'wy'}f[c㗿7o~P'xy/̍7-܂bȑL4/߿ nf7ioo'11Vm?]EEEx^*++)**Br8x Æ <$I ( x^>@+Bss3Bkk+{Aӱ~>3b49z(;v`ڴi|ݻȴiӐ$˫Jaa!SLKRUUNc„ Cxb*++)--eڵصkVv3n8Ǝ$Ilذvt:[l'? mmm,_#G`21 Ԅb!!!!!?GQF,XiӦ駟ԩSe+V`4:u*999466p€ӦMv ⭷⮻l6{9=[u=YOG ~/_ ~/߳g%%%dffb61F?d";;>}rꢡA1Oڐ8ѷH-LIQ!wQvgDD4E֦U( v͉_N$RtQ@S |@~ˣC;s饗jvI ?:={gg'}ݱ+ ~_;5x VZ-̕W^wc)NG^^GfŊɊ+w(444xb̙:ʽhpv3yy6ɓ'?-[0n8V^$Ip8ꪫ_ߪUhnn&11ٳgkKcsx ƌCrrrrd<}Z'Z焳'y( k~Mgg'?0555ֲf-MdݺuUθq;; M?mofN7k c iz&/Q@@R$I1:crrrYv-7o&77W>"''3gn:6mDNNlٲѣG {q^B; ~qox/ ~o_G:'^ pzC\s X,Vkȴ%׆d0iln7\sMTFzɽnz+}*tDKiB2GF/,}ILa2ju\x2W<1 )e.:>tinQaF$&LԎ]^xR??~xvX$|;6 ?οTk@? :ڟ}(͛k~|'͠AXbrjxCSSwݺulٲS2x`Z[[ҒeEQXX d|;|ƍ֤ojj⣏>nh4r1YhӧOիW3o;b`۱X,8馛Bk}v$=j|͛IHH|%\(>]p Gu~{ް7ERR$&&vXG6;RLQ]ބU ܡ9t;xx= ! 7{Oy{9γ>$Inz\}TUUQ__3~7ې!CDK. gEQWWg}9۷'==t-_I[[EEEڵ, MCHa4q\Z/F 9~8V|F1ϝ$Z$QXXȑ#G߿?Gaȑ=zٳʢX$q 7`X(**bxF ؼys\a%Ie˖igEEǏhi3ޞ*f/_O ~/å/?3j;ѣG),, _,EޭP?c,G S\ñϒ=Lֈ:9ziz^/mmm;7JQ^5*51܉YZZ^o WtkLVmVX,L>=UWW1{(lTlӜ4 [HFFIuu5s=Cw FjTm`ذa I֞rJFcbŷm6, W]u$iUNy1i$6V:۷E](L07RUUE~~~@1RRR{Xho]w\r $]+Wr1mHCd۵xMn9s&oY`z^k'^v->:N[#30[o~H/Kmb8[ZZ@{{;]]]!2 UQ!U#<#=cdc Xcp^<=o8,q:̟N~`2E95 … l6-OծhpS{{;6[}YWW")YZZn01cF@Xxw]p|h4p8geӦM|l6RRRBN#riyxVUN7#$߮M.:h 0cǎqkFAmm-ncǎ1~i^x-[0h -ɤ1y}/2{l.r^}Uepb 8q"Ff1~xl6N'6 grXhgΜ9l6N82zrL~kQXXȶm4hiii 8؈d"55EQصk{rQWWСSjkkЦ_OzaXxvC 7 ˵{!S$9B߾}COLLΫ jXuj?5 N;w2|pZZZfJ7ZCBF{b e_< ~/_ ~/Q ;5㡪kb6FLLX펷H-1 jbY_vS9['?On܎Εp py*Jdm6'Np̠{rM$~nצqWG$a^>zmto6űB_Rlt՗Qs?ǎCQꖾ*Iu?Ȏ;iQmjkké}Wx<mY wtuuiKG*§~oMk@F[[[7`$~un:,YBvv֑`{j*&O?.^o/--[nwwo׮]Z|8;::0 QpuNGG|МCO>Z'?AMMM|\EZ_a0gXI&_r% >t{^Oο/Nщs,G ~x:(~0\M_ԱAAPyϻK72WUM&$~W\.5^3_-o}[Q3ԻG(HC4^gΜ9Gx8q"K.15ʠ|.zo=-))fdYz.& “O>I~~>G]{V7I^,6Y)ڃK&_u9qZ-ceeeqIKKd2Ö-[{gXr% ,wމ~-IR@<g?Y4½ >|Xۿ.O IDATR8$IӇ&-,455ѧO-n^^DMFmm-Ӈưq*++)((.,,{2p@ @MM vb8q?+Yf8ȃP]N?nbb"s=濎Z|M7q]wi&|dNS/X^FzYt;|pvɶm(++8UQ,W<^烷h#2 dffRTT:nf g,p Z=+#zlGIǶIq0䝃LIHNJ䃝 <rVUM&\pwLupKV q?Cllu/DG=۶mcϞ=zmַEbbBfcԨQ5 ֣{BBި~FF'Nzxo걮.ZZZxzt:t檫#''M7~2~ymϙӾc 48/54(z IKK p⫟*0]|Ƃ fرcBGGՔ3?8IMN{{{@G,Mgeea6ٵ:aC[+K$ٻwgmm6UQQ`$''kkT 0x&I]+I{!999`0pdRSSٰa$!!L6l$, Nׯ[ݎ,Z{$2;ơC{Yuuux^IOO$S=VXXHuu5tuurʀtSSSr'p8Bޗ=dv;_}#G<}EK/ k_ ~/I HkopqD,:G˯fۻƄT-5/=I9qS7442`@TW:I1e ;9zYp_F^ο[e dYf߿[a̙3z+?6Hvj#݃|tC=v>h@xk|*&BדMWW;v`ӦM\s5 Ұħ~]vQSS$IyW{m$ﯭO>aС"Iiii!2۷og۶mZeeei&-׿h42k,9ի$ߌcƌ>F2Ǐkm۶m#11QkKkkQYYnfөfTҺ]୥%䌥|znD_𫟥<#˼<#<Wpر5C)nzVp8Z5u7mj<<9r z6T|,e b z?%º&]7`bGmMFn\ MF>r+Vꧺ>gop'Pon%K8񓓓4R,y C`]Էo_OIIIv(~@EE| 6Mr+]}=znFHJJU,~,}:AqQXV((H"==]Lp6$3pwR\\ %\‹/Q_OS~_zYt)w3gd…̜9I&1{l֯_G$)O?]wEQQQ7lO^^zHfd<bݺu|{|R}Ǽ+Oɓtr >iiirmi64;v׿^F2?ÇUXXȁi:HIIᥗ^"==R֯_Oqq1ٌ=wyQFqUW1sL,X_ cǎe„ <;;ӧ`mrFYnjJiiv^^#11I&}cƌ_W檫ѣڽwXÀw5tP &|C >b}A%ᎅ:/__  !6L/z#| DoLvvzoZ5'GbMDƒv-քtڀ?O?.s1֯_Ϛ5k#tdffL /hԬ1烙YV!s,ur<%S?997|ݻwSRR$IK :3={{"ZA,[kM@BB~!w惡aKDgg'˗/G F[o:{n,t 6jjj()) UjoQuv{n7OINNNjj*=h`{[ZZL&&zթԞuE['N|rfϞݣr:ե]kFv۱lQ_|Ԟ^j9+oFe˖$$$hkvk=b7 ^4q} 5jT\=g;MO ~/ {v~_ ~/On7[lax<DQ|KA멨`ر1䏹],a{ӯfrG:+^믿mYHYk\w{ut@‹"Qk,okCvۮ?[n%lqbb";w?fMMM믧TiJz3}HDcc#^3f`4SE|w ^5],?s0h1N;NfsxJMt^Y'Ih> itC_WW9At:ioo'###ຨ`4ںw^(//_F~x$ ȑ#<3RuN8{^}U fd2qz֒u^r\ӯ_?N'<=]#p]ŅI4 :~ZR'2^WAno8MO_y%zEA'IwJIGMddIHx%U}l_|D 0z*`hڻw/C 9|<,W_}sNHa6_q9ľaS/q8~߃?Qlii:u{W8%I y"J$i {"rZְyƈ'B񷴴pf̘-|Oמx;$_x%_JO[[[x<8q&233>́HOOQ!մg'.I+2;X155(2bk9^)vD> 6#F|r*++x<  }g6le]0[a{sTO' {_:&? wFu⇻o" žp>^߭,\ n+ `0Dm$LfS Uz̘VĿП_$Xz6#ˆ hnnfС1dVߏ9x(S焲Gh4q8;::o>^/7|sG,MW;IRC%G MFںPگ6$CAlW@P$Iy5g~Q[c̙3׳pBn?FSO!g GI_RWWGzzz\Ӂ477c0,NG}}=vI&u-]RUG9q4jIII뗥+5U~coh N//ZzgСqM6lXDKl$K_>^=M?ܾ/{Y}đߛjRPPѣc444`٨˾X: B1ˣNwu]0 t%I)Y>S7n\YMM ZX۳-ZOhw_ ~/_ ~/UgX`eee1*?i}Kd=$|[O鬉+]={G&騩)A+?DUUiii9tt:]T|o.ߍWݲe rb\sΆ=*޺\+˳eS:mOJJ:xY&O,ֿ`\}1?I(V^MYY]v, Pnw\V \V؂_ ~/7Q_ ~/?~//2|ݦ)~1 I nW=ζD_ XGEE[t :jҚ(\.Okk+˗/'++\j`8fs9_ h<Njkk9~6sq:ZXt6fψ#v{bUvùWgæXMn4COX.]#-[ӈ|𭁲{n8q":.$IBe>s222HKKy!!!!!!!!!!!!!!!!!ot:)))vns4F:::8z(ǎtdX߿?lL&z=mmmtttt:E{Y`l6cHNNrN\sδ='uVgæXM=X=ak#ҥ1;¼o>W\Azz~C :455_RRRȑ#Ky/Yq8ί۔n2faޛR=N:0H`h4v^_HHHHJex<= ^s&U=+ϵ<6Ūo ˔)S{G1~ZɠAHIIĈ#ZȲ(kuttvvRQQK/%++3&$$$$$$$$$$$$$$$$$$\.WFVMSv Eֹ,?GMl&#IZZӧOg| ׯ6E@kk+,S[[KUUÇ3l0BBBBBBBBBBBBBBBBB?{w$yyYGWߍѸ)oQ%S[Xڎp7֎qlqf؝x7<kfwc#na[t$-/@G}uWeY݄ T*3}󊈈lO @,--155 gΜ'I.͐ T*e""""""""""""""rPF(8t|;l cVEdqq?cZ7^ծO~կUUW_7sM~կUկUW__Uo7EqLT*kv͍_ȋ1_뽿ծv}___7կ/~՞U\zSK___7կ/~՞U\zSK___7կ/~՞wM;z """""""""""""""nn׻xs_+}{~կUW_~կUW_~կUW_~կUvfpStS%\"m """"""""""""""Doo/Z1A1c f1@֍9G````+~CAlor%dxx-^ٍ _DDDDDDDDDDDDDDT\ٳNy*{졻{Wxc٭^ܖjzVcqqq V_#Q!""""""""""""""ryVWW{oW xyC"^bem4u EDDDDDDDDDDDDD.C~|nl=@XdthW8| FV/ODDDDDDDDDDDDDDB_|\.9uwMܷ0=3G8vcL0XcnWAH>b9^F=Z6w8""""""""""""""7 @PXϽ@}&~=2>N9ɳ0@$un{Yǘ0$ &ˑ)B\t$qkX( I`+I*K+TU*Ie+kIA` &Wixrhطu^sYZ^%I.tf6:_oK]B8Ii6[q?k+~b!Go/b.JE]tJL IDATDaDwO B11xx`|e]ې8ðNgY1@&산8BPTIs[XԙY`\DDDDDDDDDDDDDDޮ_qЊŝ`,r=ic4 }D1\㴓 qJ܆fz0UjMB!ώ^q:ggYZYEOD_x &-{8z4bnzcRb1OW. iq92qkZqqհ>m] ރq@џ=fƘ΍D:7Lh-ӤٵLXz뽓}9~7KU83Tk -@`mA &x16K53$+шz0N8Ak|/Cc=81LbM&pF9=zs tKSFxmo"0S,cjbBB|@< ,iw Hx|~cX{!`xLok,.|0pe06{fκ>&uyo 7p/C0Н5s~fV}#""""""""""""""7kdfv[w&MR>ζY*I4-,4djY(qP$AbSber|}Y[= VO']6cG@\tߘ&m5|4kw`ͺ 16c u{Ȇ{wՎ{9o=ӵypa<'$Ӥճ"x/K8z 銈8 LJ( Ory>qc:!M+mkQLa=1c'G06`|t?zwhV1= u g -S|3k @΂vk/8`{Em ޼o?MW# Yk>{ N\;,wd5ux솀b:ӹF3Wsc|^}ysm YQ/"""""""""""""o $MyQvM{Ig ƳD<+oLg,(O!n{&G 6[iQmxmh)LsOܟ_gY~~~C)IzNjȳG(=9h{ >o3ltg='0Gqߘl> IbHZqmC3n;CϦn4\&X, 56%`:a>Ӎ 0ic?qǁm<4[ወ\C =q;|IDz.|xc `s<ސjBiɅс|2> _b>1?aqn\ X `]yԓ2[kV;g &5`R9|ڌǛ5vrEyCsNJ*j&ww;FlY/4_ BBkga`qÔOy*sulMhLqgLd-ك`lO4.5Ŵm{>;E;_gE!l ߤRIl7_cr} f=Y'6B1FSÞIԑS\Y8/f L}v}qq 0{4GmIca']6Te`ޝ9/V{.%G*(+bgi?Tbwg:7M"<ͬsAgH)i,Y{Ӛ6% x_`񉀗eѕͅ-00iqtMx@֨o6i-~Risiۛ1䢐B>8 I_+ M0psu$MYZY/0b|g3zk;W!2J3%q:O10q|9SqPl7R{̓7b 80R#3L/bx!_4̜΀Ar1Fk8ssoL1viS\g{c<s#8,yF,k,2L<24m֕E<&7FBI0 \.UdldӜ;?(KA`atx[s|WX^-n9q;99/ 7h_Cc 8[%OƔeJvq1.N?D nG@26&8fF#Jq.|sӴiR+r$yZF>cf-ɢ'~G쉘{3bŸMww: x |`q>kH\z;wy^z-+S$ex䬣o,$l\g l S?ABba{t TmfIS""""""""""""")8 ԏ3QZa~:+e./011IΏN6k ` yl`@Xwd͵" 08$N }qКɑ?' /]H(ϱxbb`MHyd;ڃyXks=݌FRONJZS4]Y7O:KMqM暧 ࡈ!zJvvf0ͤVb Ckɣ7g}` p:k,r@}Lh0tFgf5s}<0?{8i\Ob_h9$J{`̢]k(`r@;x~s@֨CI=GM!pΐ.[b>=AhI:J35B6xd5qjOz!lԃ ,>uIWI[ IܢVo䋈6U,ЎcDSov ߄9f8vNpS4ClT")D8kbl>cf`ೀa֍4 ǴM|h( x1>m7qx܋y==MI) |( =96u]bٔkEonR7ȧO33˹(8m0:Ru ZǵֈجϺ̍1x}ۤi4Hyق\]ߙdmۤCa;$  xٹ6cb8` 5tGس?fGh\_k&k|o0;'Ƙ_? N|nyZ+@6>#uǭ4-S|ᅡqxc|~Or?ٟ&IRΞzW35'/o୅4% ;!xugtc-ޥٵ0XiBv}d>fcFGƐ/D<'6o@ <[Hib 7!=M5Ck=0,Њi6$iJU+U>hp3 L ec`cr~!+o Ʌ6ۆaLv@s+g9ɳ/a~i /y/~S؟W=_9;,,.#?@: _Tbb|tZv$ Uh4[7򛗂P7yxSTso_笍.k78uw: {4Lo\WWwL6mbC{SCbsZH ]5,O EQ\#n0] 6f.W{A#EDDDDDDDDDDD-O_ըK||}؟-~=|K_wդy;ʯO_ڀR!|Wi+$q{$מW9u4n&ߤ_ 3 Cx_{i.8#O +BzxNĉ|c @/s,9N><ƾsEzo]Rݨ⦦ Iǩ5}m쟺Ҿ)ZMH ucԚYK`Z0xuͥ"0ae1ad d}wdR RY'o,C et3秩2Q?z 6ހRO7v X\lSD#!C9JF!F"F͉>`&MXWy~ 쫨GO[DagSkR迏[L17b{E""""""""""""?/| [~_k!~O2 a>!j]E('> ~w?ֿ׾$[eeuO~/" u$=~Rowr3R %Izq8x #~\;h.=A 1 iǞJ1`xcl|{a(px߰,I9p9 Yoﲌs섡wQGis,7[k0hܹM@/=!rB1!!O1@d>I6vԂ `η1k昝kS$R7hk {"kY)'hWHW10o,Lv y]|ϱ0_DDDDDDDDDDDDR>382Ik6ox#ܲ6Μ>SG{/c|oF9~{.\h/8y0??ǭ︇Z‰#ߦwbS;p;Zo}8pCE>w! 17!טVjuVu^MJjh<+B^&[Wb5\gKxw`zFQ1Qi㎕ F٨y;lPbaz#/\`jbo_ ];ܲ?Ǿ=EF 0k cpi ;~38}uu狈ȶww{Cc [DfԳϳ^]?|oX[]eׁ+- Fw282IE];%Mb(w+ף :՛44 IDATs,r a^z&x/_dĵ5B F[fOX &6y.y>"Y9?ʑ'*<na}9B\Jx#l4 (A~0Ds)q lqL'q^kPx R\>"FtuwSoLMyџch `''$MZst峀ɥ5v<ʽwdjrWͭ5jrw~ȋ<ͯE92qwqQkG6{sS8YZ]cdwo$S R? if wu)-'6,ᯥ4˞]Q+B3ya-N׽Ba($(' Ka{0A44)zZERR^nRoĴZ-R$ e}8翣C v sc})K/ż;r9z- s&{`nvL0]򳄍Y$OkԾ F9$;Hw@y+U.C܃ @g_zY8m\vz$䉿0MO_r22Fӕu&?cq®]; rب8vԛ-&k,.RX/W71q{Kt:/W;Kё#ijewbv5Mh4Ŵ)C}CI;3qg5azDgi0zw/""""""""""""o?Q@V/C![{Of\&̃ 1XsxY`oEP(gzUfW-?<[J"*NasØH;I6W,,geJzN"ISsoؿ=狅}#h*91PmfQhɇR@S #o:x}?{wѻi3aqq&,"""""""""""""r#)"{VX[-39縸 x16 _pnM;]!=}|#}';oD;!7@Z*6,-O*ZvR~߆0A䆈 :Cra@>0D!ٱ>Óhe7; GޏW YķI2iRťMpΘr= Qq ߉ˍQm8vfc'aaiFd[l^bfn8Fލq.i} `LX 5UYX\eie2Z$IH{$MYYPy(G^{?EBo@& e6$MrӐgu,.Q.WiZ8=k^ov-,116;)u&{-ʕ*+eVk q4u[]8I-,cm@Y T*RXZZ6jVMѦn xDDDDDDDDDDDDDD. HSG:{k-A`_\u;mqiJ;^Tk_DDDDDDDDDDDDDb !<Υ$IK2ӣ^DDDDDDDDDDDDDV/@DDDDDDDDDDDDDDD^ _DDDDDDDDDDDDDDdQ/""""""""""""""(F䋈l# EDDDDDDDDDDDDDD"""""""""""""""ۈ|mDA6nn׭^lcg)V/CDDDDDDDDDDDDAݻK,,,l5F䋈l# EDDDDDDDDDDDDDD"""""""""""""""ۈ|mDA6 _DDDDDDDDDDDDDDdQ/""""""""""""""(F䋈l# EDDDDDDDDDDDDDD"""""""""""""""ۈ|mDA6n*8f~~$Iz)"o[DQR /Ӆ x1|n׿<;wꥈl)ֿ2Tr߿_n[~4BDDDDDDDDDDDDu_;/p^ҰzwTz.<կg"""""""""""""73oҕ|Qfw__EDDDDDDDDDDDDnf ?/~үW~T+zA+8UGY+_ Z7 Zȵ ]nxA:k}꿱\Rq;8MYZYԙsuM"ד\npZ!vU꿚y#w_*i5"ן:\◎ԕο֟w)s/"""""""""""rkȇ<_Sǫ 6_aFSz/"""""""""""ZwUTǽ})ȿF6|3_+^W~=y-kt)ȿW^j7^^k4_zUS_ə#Jݷw -^ȵWo4뵒:mLAA~֍ :4UwwZX;X.1:8ݷE9~aojW,"""""""""rmzMv+p%!Akzw׿^zT꿜E仝_z(gLX" #0Xbj|zl?Wz""""""""""Tݥ-Xȍ7ڣbz_RWR?S*J'> s*o&|#~ר~No32uox)|']7h""""""""""WR~O<}ط<Ng㣺|g43*%T@HHtbl -&d77{#nvSq8ubc+`zD/B:MS~u*x|Ϸ9P~ڟoٲEP"ᯧ7n[]UTT|>z)M8 /(44T?Oe2OaaKxfh@O^WR w*ǭC;~->UTVknR*ǯCd0znm]N?]wok@Vccvk̽!11QөS|M?G}O;~v<f߳gtA{_1kujnn~57SQXqF5T+3%keSTRg6C`MDd2™R湿hˎ p1#]=z)3zz ނٻwd2zuV=C w!m۶MmmmQEEl٢+,,L&effN%%%|Zz^JJJtRDf?kD6me6E)$$,NJKK^qqq)I:qℶnݪJEGGk1cUvءb%&&j޼yosƍuYk񏱼\w֙3grK*66vL~}?}RUuk딗A)>vRa!>NKfYFo֮=E{,E)b宴6vP3}Jc,ydce4;ѨdX?O)44Jt&e]r8w Ɩq)z W6/>O )Շ@uJ~+++uaIҝwީ0mذA6l?y =zT7o$=Ӛ:u֬YիWq_"ɤ'PMMvVV>Sl69N(22Rz*((P||'J-[驧REE?SJ"""Μ9_ޣOaaa{b k裏;hٲezze2t]wK_^xmܸQo?/u$p2ףdIReUM@ r_Ѳ;sU5ԔAp_cԵ[e6[.lQ}t#!2"\_K3HoimSm]Fg#?D}-m[K >O6mRDD5~xL&]Vv]uYVYVM:U>sJ>Z_ߗ֏9"Iʕ+zjڵK h/vUXX"9I<&tAFFz뭽. ɓ{rnYswrrڪ[R=HbVyUu@#'$IWTvx#'ti )/'KS'Oƭd2u0sb%Iu *:X:Y,M4Q3}6{o!d2)59Q70_EUյڻZoPdDURZ6ML7zݲC6CsnWڸΛ rMLMKΕkOAUVph0*--E3N3%:}RSt1Υ=X|LPRbXx{LY3t 3z?ʊQB|j+ D {x@@C vw)w}=͛ޮ=Se6mڤiӦ]>07 =mml|}=bٴ4nkƍ,YzKڱc5n8fl6?e6)pLeZ+xsAhh?r(PRRgll233vs_OW#K&'* JUEe-"8آ@|iyva*֞}suۜ;{y98Sz5 VFzm6U8x55hϾ:pPq%%+&&Jhw=)ۣWED+#-E>O-os~Emm6 [Tá-;Hǥ{RuMwS TF?xDdzU[ߠ߬TUnw`1X|^Yautd6S_::\ C0Ғz-ƻPLtf8So)zz7i-7|y>dj, xD=EڹHM-JJw O6Ww wj߁êoTfz|䋲`&8uVn[ܪysn> >򇠷g_%zҡE4azpp***gl6%''ffR}2 JMM޷o{ wO~~ۧ#G(,,LWHH̙#Gرc ̙3%IŪ`3O.$)11QCxB|BgwvZtic׶.Zmdv! Ԭo=e7uq-w$JRlEz^~hg'fBCCN^cR?uF˗,4>zB$׫oPcSJ*'F&SfZZZ_^Sɺk"?tDxuwT|\;6ls5%/[6CAMM-o+k|6lޮxW'TШ6l}H1Qjn|=EoԷ^?_]}8\}vqݵ랿k{ R׽[*""B'OWO?~i?Ν;5w\YV鷿/Ԝ9sdXkjhh"ͦ-X@>O .Tcc5k,|>Y/,o>uttԙ6>}ZOVIIjjj4w\tgUeeΝ;Ǧ|Y,ڴi*++uIUWW?ֿW{׺ 3K*$I).6Z34yR>&sUZ<ܢijlj3%jmki w7cj$Nt|l6nal6aJIJPiyU|ʟ1ſ@XX%j SfzfZ. q2T_xeía8!C KJe4e64!30wʽiq ׫*͝reͭ:_^#=%)rq1JNJНi֌ڶk_Ѩ**2B{)b_TsםTAI݅9m̛XewNnwX)I9m"­Z07Q-zrunո$^ IDATIRKk:::d0A{\a]o3`l x۵w^|>͛7/ llM3gh֭;HouYg?󷙚o'ݻ?ٟ'$$Dm\ԩS|EGG`0hʔ)JHHP]]㕔yŋ駟s=l\zڲeΝ;}[??g՞={gOo~$7MZJ?Orױfu'dH OҾźq4IҌiyzwT]Sv^2%u7lю"Uw!<){ ˄u. ow޹xq?VC\>] DHH䓜ZhH|>`NgKUVQں?uF2dbgfݮ#WLiIC*{"535 q=7W_~0r{oQ1. 64>#M 74j=74JUl8!CKoM񮢣"4%/GKnMD59w/Yؘ~ N/@HYV}/++K/ޮ %%%)44?~ժ}{PUUp /{`0'Wp(66V111g[Vg?SssZ[[HIRnnyVJLkWU@&MW^y%H?VMM\.H~W"`uV&Ly>{N--mʟ>E=A!ڻ-f3u ֦mt|8sLAZqvYh 9b_yU -ʪ~v ;{{h|VfBC,?}>XIW;Sλ=N4v7~#2:p!IUu)ӿ>V8*z̪N6Hܪں^c _n̓:NhȈp=WVY] 3íVl=ZÔ^]x-;{6mۭ^G'Wu z{zwo?>`]Չ_GPNi?ժ &h ^dRZZ5 P(##C&MR||?UddcRJJJeb2??_MrstB; )=-\zɤS<]LLoU2?fEEE(z/ۻ] ^XSXΞ+SB|\3-\N3=*&(ŸPHH4KFA{\Vd?r%Fg\Ol~z*.UYu~~Eė$ۣZ[$IvCNKEso% ǣVy>j3gmjkkWdDR/]5^֦)y9sڸd`^OUxUuʞ㦂ܵd^z::ϯꖛ|Y] O*:xDNK&gύ.͹QvhԴ)G\ iyҪ}G ͝+%ttTT*F&G_zIC[ƪА`MѱTwzݹϼɉZy]zZѧ2t%&IZVWL y<GiRwuǟ`0(oD=:q^Z󕚜XOb1+4$X˗v>JhCepu"?DC Lubڕr^sײdYBpk洼})IJMIREers70CeڱPvߧ$'si.?tT|"$/|^k?T6'n %ޣGT$l6>4@Լ}y=ZUT콏,s^Xm:yDGOII "lEFFh֌*X^W7Om$D~Ml#uQ;o~Fh|d-[ ̺\SbcSMm<2h4*2"\3Ҕ5>C qCZJ_e RHgrs/?^7̜SҪe-V޷\XVE[e6%I7͚fMWKKdIfN)jo+22ۿ}x+a!0fsgcmA]^7ul}gnzPG%%%ʺȗk_ᮏ3ޜ9sFǏR8WY{Xr&pkXEn5t^\|E^W M RtTb`(^G򸉮UW(ɧ[ѡ.EGG+"ܪ~u Eu{ \Mgң__VȪQdd12 M d:gy^vTR$$6w<.^{|m.7@ W|3 ^nWW>*Ţ~ͦ>[o4,?ѠV@٬+p\U:{LԹrA|:ܽv%ōv7^*:Cy;czcR\MzC5HBCt]wjRQ`n)@0ҁ5a$F}I:\2 X,L@gAϾ_n3c`W3R}+}}ki^_n/@i_/w?8 ^+`J{]/?_A2Ljii30Z? uc|!C0` !B 1@>ci;0JKKG FMhw"1]0JN.C!C\Ξ=;]@Vu0p͛4ihw]v4` !B 14W>O M|>(9)QFAx,)[z.٬\ Zu_Zd6~.pp GcdF3%zjhlH#я ՜ٳx\Hm6[nG[k>ڠ]{/?"#‡oPW׫o7kFRGF΂}zKRdU^Q#N(:*JRg9}\ݱ@A-m:|>ݼCJǿg kmm'7jBͿuv<˵IR>]#Ek>ܠ=n%%놙SGFkrngzO{RMʞ0b}4Ia͹yfs@6lQPP<l|ۭZy=%'%*8#RSsl6-%l6󩦶^vCI UzRjJ-=pjhjV[[f$!,%p\vClc|`m޾[NK-[ɹ[tHU5򇨭]t9Ziv)҂.ϝ/gtyZilf8tTo?o6tK5Y<6n)rְ0z [tHM=Naͳ? ?ȱzjkIFX8OKϗؙ;[v^XξAg497[A>׫ibV|䋗|02#Z45o%Q[ IRݺn\ OJmںKn w(jⅷjK뗔luB5O(3}R%u.IÊW]CZqn}M͕lѦmkOr'hƴɒҲ 7|>k"۸MAX Ikh}H1Q튎RHHrstt<*9].dرƥ3RU](LCfavY”5>}zxm  穹UEHjjU|Λ`E!!3?d­V=4.5YSr{>'IYG[Y㕒Sr9S iz޻$IG7J=Aiw+1>NncIYJNJP 3Mԩ%|Ϝ$M>f#`0u=qZO?$%\׳K⇅(1!Ni.4k6o߭`zˍ<]gUT)6&Z{~Vϧ m791^O*44ĿJ$++W\lbM-; TQU~ۚ:9GAAF<|Lٝ3O=XF[ck?cSB|Ξ+/ޟh͜>YEV%%)3#- ;:ܽU Q$HϟW/IFAok?ѬS_ `0(3}N9* :v⴪jj I|:v-?&NPZjܫ(%XcXII rJM>ݟ7xCk֬ф ܹGkk駟򛛛UXXpl6bcc5cƌnm>5ėSξk ?&:Rw/]__mMJKMS}c;ݵtQefxXe|)6ƙWzZE 3%i_^/X9NWVk]`b8uFy--sBCBkOsw 0/^{GZxe #@UX6+ .Գ>Rо]˗/G}'Oӷmۦ4n `-֟z 曍Z4ae:[)Iz{GxÖ}q1jnnr ?=-U~Z} y)z [Z9tl6[,1mrz,f9Ny<^>Nzֻ[4w QP$%;_{Tu^~}ug-oҀecpCק2sgcmA]^7ul}gnzPGcؾ}֭[ &vUTT??Iz{xDK,Ѽyp8LO<񄊋USS\O}_ӧWKK7Nwy :qZ3r3GtV2IqC!tԤPEh4 \hZZp8{InTBBʃW\22 *((`o>Os̑nWLLO%KH^xmsw彷Kۦ |ꙑ?FFeee544c|!C0^F1i;:' 0.*..0va³e:yBw{GyY4!= U@> #|NViN$XFWzcwTpd0hBZhw0vZtLf!FEhE IDATgي @>`4XzF+|F> Bb#`0`0vW./]\˸ 5.ƾ"_ZZ:]0jGedc ]E ?##c`8W=]A0!C0` !B 1@>c|!C0` 1vϧ54Fmmjii$EGG+"<\IJINbՇ, α08^l*+3gez%IMMM*//WTL&NR|\syX^g\_^F=557%|=z}>556=1K111#ap9Y;ccd2iܸqZtRSS%IGՆ #(***lKK^|E-]T)))zz~joo׫fy<+99YsUjj۹{~}嗕E{:::j*-YDC<`:}挎;!.Ww|\N)3#c; W!γ0|8< ǬkתP ,Њ+t:UXXUVV^^233UWWCn (a544(##CNS555Z`yf$vZxRSSetדO>9v\.Εkl6'OzԨǻ0󪦮V[VUqqU*q9YD  *))QAAV\ӧsrroh͚50a”g=77W!!!r: &hĉ}?c ׿־}t= j755U[lt:~nFM0ATUU%Ţ |>?_ֶ;#""n Psss@hL&544J<[[UYYX\<ˏЉkY^g3;ja;pt:禮|>o(WuM|ݞt:uyWV(v{tRmm=L.Wǥ 555"刈MҴS5%//q>112yMqi XN+*:c$ǫ7yx<ހlKX_<w߹Eϖ׶m*-PGGyZ%dه:$7tTШ^.K׌nכse_]9__ݵr-+]_n80kfwfp}γp?UWW+));e4USS)44Tŋ%ITxxʮ[N۶m7o^=Eۏgo!c}ۏ+1Y+?ZYwSLt4}Jzn]{އ=5.5Y^Wݟd;=x]9m0ّV^Qz ׭y<Mʞ{Cq#ޟ[k4yDrS(P}CV޷ܿkYϕ]wut[{wˍ0kr-{=fP[W?n̟;w-{9Hn.;΅7,0ycsh`j2}iѢE2 ڷoP6===`ywIK/$'ϧ`=\'yH'duֻ{~\FQ~~%wSoڮm" pgGV!!jjn;k?_^}[o6պc᭝﫪5!se5(2:܇\+ײoW6o=E>^k?""Gh׆k< W1(11QeeekjjQbgAoY;wӧ=s ILbN")ʔDKlZc{U3}޾73eY%+Z(b3 ȹt@{AOUWv{ 8N⡇ ⩧ӧOgϞq{un底455p[ld2QUUűcthFyyBEQ(/+fQM5ͭ`Sc | 9~,[7vkdy2Z,utI"_4m3-LThd:0rCq(1KiwLF6Y}ini0?흄 7L4vq%u_]=nzz|mǓ;7@^nO=>=P~-bwvэn7 xw}£ףi xG1Qp6!BӍ=qovsJXr)z>@'GcMGin֭x7Ȍ-eewv#'c[7I8|1~O$IWUm1LD1`7IRlްMZ~E:+0~^GSs0ٞLU_-{'z`YwS=§%~lNg0Q[ TU5zoט|b1l6tʄݏqV!n$gJiii0_5z,;;9sP__^֗t\dgg/s=h\Yǁ%7wd6ŋy饗cJW ~ vG[G>md9ٞ1?{"dsn!,UiZ d"<K=F8b;wy #DQ\gݜ:{Hѽ^x ";~!?ڋNcC׿na5o݈dy_G7Q^V‡ʿ:~Ϟœ&Fjsɦ|nZaݝfՕ#0sؼajL C4^ji]` On.gۖw3qv6 ۬H0~"kև9~,KvV&yjT%( czHӼڳmldcx_GUNʯ/~V=XL*ĝi.^wX4: &Z+.x+^>[I('C'\oh}ч;-fZI9v 3FQa>ŅX,fx-^g'7Mu:rsv99|fg>9J xGp8Tb\ja |!M۲73- W}7{;}Dx]O~Eh8|+!8+wJfR>,w^M89NvAmmϛ7ۍNjsʾ9rqFvq\7}nYYr=Xd {!ƋcX]X74{` igtLF&t:sJƼ70O1o~i9v˗ij W ii렷G ijicNi1GOvA5FmVW X'ѕF/?vD"j] 엗 嗿yGl`qC1b3 J~~KՍ;V[UUH+l{0L8vôys'm308()-fރi(/+;W㟏٬‘sJxt:~d]lݴՕ_ mV:C"$M04|ÕFn&Vo _{mqDUˏ_?JFB\tٙb1~‚I_;8䧴pLbИ= s>vr`zyǷ?Ƿ>'3c+TU3؄45a29z4ŅcƟȝ뵃wGqAm#[#zz ܰNB- L[ntiG7_>BOqV!$g)֭[ٲe CCCL&N礯t?G?u7uֱnݺ۾\k_ڄ<<" \!D7/ቛ~餻E$NɈc;|}=JCF^;z6oX=i'Ϝ`0 hn`au%73ɤ?x OԓJɺ M._sdӿ^5+Nx>!2n.nXWUɜѥم#'k.@tҘY:nVXk].'!ӇnCczƽn_< ϻqJVX_oN֤W8Oisk~Nm]dge0Xy5Lqa>=d{<\lj=~M1Dwo$Igg3^r:hnSOHd Աeq6+_A^}=~k?3ͭtt2FskMm]N[;p7LI^;j9z40n즫OB܄- J[njb|E1gNN#++I|!*kst- ٹSkWn+PUUUimF XTS͐ϩ3QU{P\ONT*=:s+$B5q^P!}H$?~^ՌE5|0A%4^j!VI&S'!TU#;C^_M)vt:4MÛڕˈ_wtjWO/ycJw(,#3OnO;wc63G'UxF2mm來ˊ޷z?s!D\llb8ݿΜR4*?yUp(rޙ@7IOW?i ytz`d/>~6[Yl_z1`7okto>.45fj+- ˉ _m~Ww/soXb˯CgWd2E5lXγ__{ ~s0Qo( R)f3|sBLl:콠 ˎwvֻ0ϖkGu IDAT"o_AQti,[~H&L&#V0iպ{pcI&S(ܶw+Bܙ- _ڲwgG{9yVH4F]MRB{B\K4/Q&y]zk6\kW^0M4 ͻTcsG֌߿iJ$bq\WDc$ \.瘽c8xÁ^?AUUBaW+HMF`6n+{f&MӸ3gGg+ vNFnRg7N~^.==QTUf2o^sRL 1 B1˒ +_L& bdaX'df0ޗJ BcH  d3鈳3ynj!\.DŽJq:cbS: ۬cڧHfʧڝB'm`BD123\^!7g;v;ɶ #}gGgg'. N(:tEQFWO4VZDQ233 pH^\k:횯ʴk\.}FA^B!1L׽ѴY-جqy:7OkaWLTky2Q%!hG__iGtrUK#^nZ,^/ޜ{yDFIʧdrqOw`:DSpMz|أMy5$mʧڝB'm`88+DB!\.P xPI%UR&|O$BQ[QPPyw:^"qV!X!^gb$B!ČP(AD#}8l((, -f!d$ !+KBH"_!BNGQa!L{zinna8(PV/ ֙A釋É_0QXBF(LP!fZ?a_B!fITUE4z~֞O!!8! Hy; HbN1" { )o`Bf0fBKN>PW8( ü 4\V^Maa! ===?77;v/ͦMFq>CΝ;vSYY֭[Z?k8Nm6]]]|pDؿ?ϟFUU<ԄByο vʓ7_ܖd!5:8O!3J[oE}}=6l`qWUHRyf D" ]،]*$& |0N 'D#s"֋CǡƝCCha_ǟ0ΐai#˖O-كAgO7ZhzL9E*=ubf}Ew(tLF :Hr_gB3V$t u̗&L\MrwGp+ +ɱsݻ I5!SMB(;tM4/ƍopp8Lnn1 6o{yLMM W&??'$1#=v щ`B] Efާ=JyBTC&8ؙŌ|OblIR䪤]I ˨J,& D|>*~3֯_֭[oB!n]w{>%d4gb7X,Vq{7rU !ѵ[*JՉ5"LS1;d΅/$ֱk2N18P+IwM'(ˬ=\G [֪& 133g `HdNJM wbԛ[?=5{BMtV'{vMw!Yar4g0e0g€^?4Ć@og#c\%0ےMޕY~`W5hg`]K . |a&{$Q&f9 }Dd~l:PעȢ7V$RqjD*F"#L'.'쭘 Vz & <. 3܂yvϴɲqܖűvQ LIdFg5IZKa[pjFѢtĺKR(,Dɮy;] ۏ  Ʉ;ӾUǖO3i c_ PBnߩZX*B4?X*© E(\˜E[OW l0XuDB!) = UVVR__OKK eeec8p<.]^^NEEŔ\ܹ9rkb0|O$NsaΝ;ͣ;wNz~EQXlvOBYITd)tt`^;LiRj98͙b>‰ I5N`Rf K6\7v,m4ɯ)zU_ V,ב &т\,ƻc8_i 5r.s[LWU{$1iSS4"N \s|vS0H|*)5G{)\/R1" 2Y9@h/TH2D(>DZKiiTMETTF5Mla1GF;_;+ZrN&ne .s5yP\}_Dnb{߃`|y",)LtʯOGgۛ`8gw5zנi*_l~z3+g'D!Žӻygٽ{7dɎ;k\.{9|MM(++>׿ٵZl۶W^yh4ѣGywtORTT4E!nDTT-EJM 8EQNN4H7O{1ro݄|2k9ufN̅W5P|r`lH2H45L$" M%hb0ZTC4T-}\<抯uL,0=pX*L{s2kF--x\sw?[>%)!MH 5w# Dl<S:c>h\WM<)F$92=y9+n|*t4pgHGM&2^3t\<Ɂw,YuO  wsqx:k}X2'ȰDY$&9 ݕT笘Nu{{_h;x*¢̞)eQgup+M1͔eu|ܬYΝL[uyeWi78?GAQFmt`3:X Nbah8N cKט=03o֡]fa2X]Eݻ9i7|c6H"t\;P:l3qw\dQ43)z >үk7][;?b),B!y@Fnʖ-[d2t_.vկ~u}{<ַH& 4>{ꨫ' b3Ji$8O45 tb:MRZ9-Zrgh{ UK)rWp?Pmmɦ31UKM I$BDA" 4\!1YcMEZNa2ϻbL2̠7i\&UF55I9cH"C1-}G9ѽ%[,9jzB-ʢ)pOIo8͙zqtەc/⑊o.q@yDM (Ir‰  +zz[8'a.Ap)fp"PB-~'DjMS@Am Ɖ|oR,/FVKpS֕>ݔq+xyT7{͚'n }ɱQ{G~u17`3\،NlF'Vci8K/?g†)Ls IDAT |i cgwͬi ,i8]|N1"12JAb猪Խj]UXvx¿rq(̷ľmyhr%sg2[O*佋fwX v*|qVB!wȟt:YYSѭ2xSffMɹviFp Mi8F_X*LZMS8+ "rELJM/psk\<#+n]9eQUM+ȥSZU姂Nc3x 5Txityg+{QD`#n*Qlk]YI$l<$B4N~EQAjAeRjrrK/Q-X?gs,[>=-_ wؙR <^Uwtە,c}3hF)Iosg1ǻ?,gV5$1obSp}#pgtV:o{PS9(rW,HE9;L2&w-f{?'O Y&ǪšwdU%O@x*J{<;i9%n@=|9F+B$jC\8پۈ&C;IKrOY|F;zňi-9H8wݻq3XZr^:4xV*<(& 8Q1R$˖:EOU%OjiZ m#Fyt.UKsC";̆/Q*&qH2DYf- 2,,+؊Yopǻ4#8geWmq"AonrW"}4,n6XKw Q֥1G:d :#TU%ۧdez:0Mg֑BAlId(+$Mi:ũOh' J'0L\:XWm'yT-M[֡3d ]9NP;L06m,-r[~2;0gnKxGl9[t3M,Fp"N3t.m/dnbLlbadif\s9;"Qے`è7cԙ1L:uy8ֵsɶлK$1m#V8$EQp=Eb#(eT2-fzSnCӖUS\U( 0>AeynP1pLN~(8!s# #(J.?d|*J}Wi)SH0%) naZ q>My)8JZR=#D2!F/0t^l/=$DMf*+NM,Oc*2an& S( 21fvnȯ<6duPuxmNjD_喘\`m톺Kf"CezvCw rYFH)[UA$DgD"S+_2]1 J j_ǡèWZPZS=#\ |̭Y7no-\8g9:lGZ8EB0z\)͕8RD.URoD)ӠU97Kɖ{ml1/65 "RAP -~>UcDar46.!LX5.L*JTIopj\C[K[U2gh y=^e\)cKo3Z~&ӱ$b:S'U>JJfkjw Ŵ-?quPN1|0 bZcL\ctN]= Zcf(|d!:؎kZv^R%W?b6:̩ߑ/gvd Ρie)V/v)$$QV2A, f f0|Zc;ۑJ6__& ] ~L qfʁض[L01FҏMmmD0ra&rRHqaBkL\'%]D#>gS?c%{)uiu$Ëo#ic!9.éǠn:Q}&zPrFk)Ʈ K@02sQ-=*U(WP>]}D`V;8=+7{ V$D wCD"HtOjU6;Ybb:W?"©Pݛ4{( j*9N85ltK]ߛ+\E 1x_u+ WZJu`S{#`2rT̀:y[K%r7/\dX*~b6:td@btTNμ镛w:°YVd1ékFnþt n-]m^I+ǘ ⏏5QoĩobR9x]62>/6/v)8t~]s .f6z K_qj$rzz]Ǹ-7BSo©lLNWZ7uQVrs_3|Dɋ-Ai/e %c1#_1bklåo.A$"`6:MFwZ"]`h4$^iFnڏL@&Q0r3oS(qT$ TIe+eR|d!F2B4BjPbLŴ&Kk*U b0t.]=\he9\4l`R6|bIW9GM0؇T"j)V+LEn2"" WZq܅ЂEy߻ qj!q\)HL!j4KuNJ3+m.Dlؘ- ͆rԵ%Pv2TIxn[KȀ+0G~qcN]=Kir7H%2{w{H$D;D"]*E+moQB+\ ~bʇWitEMuxnvڱR_6 8ȾWWG %Ǩox|SKpZwVL kOO/etuZ!SL1 Pu4[vQgԁAe%eb:ËsG){pƯ[OP,[1o޽K@}i?mnrG:W$ s|ۄS$ jMԛ:5oeDbV;9\2Qu^X3%WP4t;dFBde&Wn0`&z Ҵt# dEg[E0nG]nϠk8\]R5 bF %T˸Mԛ5u5n* 4[痙\KzL,_#[LM*&K/rDϽK\u¸sN! 0lׂjt!f*r@|ŔpjfkIJT+ 1nFsE-לҋNaB0-&9;+WbT8C2m\KZ·}rFnݾٝ`}ɄHc,X.z]t.h~'YR"Hkt1?>g(e\uZBrAr}t@u7g:Ђs "w ȥJ{QT\Xd z]ǰkkeLGo1b.:LRFFK/N]ݝq |1sn.clݶwÝ|QBə;--;fl{YɄ jcj8;Z5u LLG4!bna7B@b8SAR3;;:s'c5RoF6_7rLƍЊF[.U`f)ouߙ"*WJ,fZ}Vlǘ JNi5vPg܋KWb4_'vOse#C_bQhoN*+7\8M}vo%..>b8|y]Z,$gI蛱hj6xD(RDT"%`>1q繨5hclV+jDN$8ӑ! v-WJdZ<\d%tJ};R.2RA%.ߧT)6NA)$ׂbU",P4hF.p3td>D89tBVnC j7u  ],jN]rѥKDv5')NXJ&'Z~Z3SH⏏1ǤrMF4Yz)Te).HZ'v\D"@H$ZJp%[&V!*jܸt v~b%T!F=4[zO#8r&L#p> SYL :־IR~V2A?ҥY]%[ddr~L j?jAJ}jlu' ;MǖJLbPh4hͶl}Ȁ# .|^a)$$]Ha?@~}_1ZHLHLq- vm粊DZEWE.U25^7Te2$i?qƖ0C0Z[Ze?{=' 'g 2xʎz̥<C$@.7[~ O0"f.6MAJLdL`i/KJi>=J<KVn<(uh3khutCAt1\t<.}#]΃H7$r<N4\ORuԷm\G |968Cԙ:i<8- -zs2 ]{cQeuhv/93T6S[L 1Aeݾo]IBҋBD*axhݶʲNEA.q?e8|.ױ&YNG |fƶ1 '[9?ٵsb%h y -Zn-at".Rh4{p҇]LAsQnNs *:xSs:c6g&qyO&W+k|+Z3t Ah7yՄ=*ʕҝ.}r7D"H,؉;O"H$6U.gr:s}&zMm^߽rK\~Ƶ'9:y:SǖޏJmsq{VZU,p##0qq[+t;-3rϱkj/&\NifMe?uN4 #a -|IG0Qoպn!LjRC*H]./hYWVLKi?ZuÛ Po<.bqP .R-JpkW*QJUTV v/N%ڎzdZfݎCwM fݞIWXY¨}+^JC|1*/17ǡݶxnKq׺?h-㏏+eZċ)o!Rs7]8m{ &_u]wZ?Wl#KPɴ:mhV"HkI/_N;ht c4Zz fx>9̾CWIeD-dQ8} č q::r\yL"ǥoP[|9bn!<.D"ѳB7:oŗkݾϞ[ϓ!- IDAT>+4NgN΅h}|ע$6y׋V8=w-_F%hمZc1RdٵTI"\(U r=ǁ7[a1WJ1E9_Ǹ-7@!S_sCuA"{heq<?J:SeU©9?e)=Om^| ȥJ%N]yǴw?ݵT"æuN͑)$qhk1,-U }tSn5#*PT)c,gzC1ˑfjx7{T˄S>9A$rH@#mkR-)XJ3xs0pb9]㡷8/6믴 M2!#_B#7yqr9̽Hu}+̿bR$&)8tuq+|+z]Gqq>JrzcJCo:Y{Fn [JNH"hjMz qn/^V vm-/dS*ZS;B<4r#~L16}APV[Vd) fCdK)gšp[ AJŦq3"B*bzQɴwnbh,CaV;y'Վ'\B"H)dzTɴ&b#dKiZ7[ >z^ܽJtdpzLCE!U>I9}rθuon?+,?}M%>9B}WiZؓB¢%\)7Vⷹl1E 侥)VE"e6z 28>IlHMôԯvE2D\=@ py^*RX~;?{hH$M/]Dq z\Gx`bsZm{wT҅L5osנjK8g6zsp<*s}ͅT*e]8T}7*Zij+|1-_FL{+1_*GL4ڏ~tDΉ? e&:JF>U>|Thg0 _`2rPr9T&j~ӷ)z:U">ocTtX>g{wZX.2xiV2A%]= Olx6Ð Ry_! 1x 8tu+%Rs|4(}Vn4{`:E}+ש7:vlksKQG@s߂ jBr[LmTWUBiƖQt\{)L@`(%׃i>eDA`F?g6:EEGԙ:9#-}mDɖO &蔦otT+Da.T"cT'\Qyt!\l 0Z4[:E*y ˤN]=GcgghE#(JD$Jr⡻ɥJN1om N]]j$+f4g5 vpͅSx*Jibcx Ԝ7uW+\ssf9䥖?q"H$m7SH$(WJ7C_P*9}jcPZ5nK,%+^.a<[W#ϩV}Rglgr_l/~-Z0pH#t9q-TWdZ{yhv/h&̽i&#6-=[TI~Ma52:kOSoRr-bfK]CmH*c ?Zpfm)?Jqӷ)zTU(o[fʍh1'~L v)̾MTr-W936 ƵzT25y~"UmTpkOi3`)X)`Rٿ\TUL!αPg|TNaf1g93||$Vo=&;iZ!A 9^iᵶJ}/ rr66LttT!QeǢyI|ϲH 6Vnz,8TjۦvAN85R&bڏYq#j`bOA-HG< Av!3a%@jSPrv>,ȩ5bd!beN3!_ʢiPb[^і;USC"r&RjʂSW篸6ip4ê_V6f#Xh&"7Y@E0>5ȥ ZjMDdĠ`R0bP$Hmbk'DO51b/|/x96r5($H)J }񾕪 RT!F4f)=O01̽C 9Frc 8)jRL<2},fx%@6$ \ ER%5o柋psBv>/Z[ɥJO_д67r0M5NT,=o $&+/iB)SRBL\# 1bTYaiJf~Ǔ_|\1MM?p[ԙ:uH$m {Ai%WJ3"[" s= sצfVsYYn#d8tuڬdœ;8h,ק\JH6r&DZf)N3tGα" ʍ qb$Mwv=سȇv ƍS_ϙ!p % ȖR6j71Fɖ?Y!n.Kr J3Ta*rXn Bt) <#ƶ-;& LC]5iWY($V2!fc 1|t1\D0C۩; Hkk XHRЫ̝-p?-u}dlIdشnb,f%QTnӅVZ,}0UNT2#ѳD D&@%D"2$//ɕ3~TImͶŋ, I(̎. ,bcJir4bfR|)²|\-S*ȗX.^i'4Pԏ=$$8t}4+׹˄;yyS\TIuo;xx $r<ӑAt9<<:H-]Ʈ0~APJZSJݎU]R v|8S2|>sk=jgPRb!9'S|bKZ3(-J0rϦ/[V_(&e:2H"!_Jh.sx -Tv2"aPYpg9d:rxn*}6_K(\O,1F/`zq8|t!І]W H7i?Swi:e.vD*Ȩ53P|\)?>Ʃ_Z?Ũo}= ׹EXd򯙍cPZP4D ,^@#׳\F02gdqɖR4[qw%:rD2!EYH2BjO0ԛtǬv>"WZ}t!LdhKJBrLKm/Z$>=܆fvOrjNl{>tTCWse.? 5ǰH$v&1/DϠl1u{`Ex_äv5,(pz3s],MgUUfc#\ 4JrDU-QI(e/ :LQiþFK϶U25ujNp>Gk=B{)t;Ꮟ1r3E/~DNC?'\uŴ+-_Fg6:r&Aiɺ \/ȩ5sO|gSsJ61 )WJ|9K|Q J+c-x$8uRʇ/69߯9otبb9|| ⷉe)U ԛ{vĥoĪA)S+kRK(9 @k?nC m Ld,f\ặjku M],f>K@Ze*rLʁؾ6ٵK0WO1IFOj7wazz4r=mJiνEtJ5 hPHt:*ĸɕwGɅO T~$r+DaMxm("m30JU66Zmep=)W"cir s+%#1(m(ej#C(*kO3nIF&K]e% #׃1A"SHu J+{2k?j %41+WJF<3!*ߢqw@$g8{ҊC_ēL3 !7nۃ؎SWbT9N&K% jKfK?omd>Z?Pz~Kx&>>ݗJA\LKa|Ki? ̽xSr$[LrׂW&=碧^iyT!iT3uhh*k|/fm$#1lU6jm_J&H,K DYyxnRk.}#Rq}yZêٚ2@bShoD"H$zĝ(H$zF#- {:SǺoC%<ͅS,}L\ŠS}R .^D*8.s[.rnaJ]-rUHT _ dl S*yZi0<ҠH0fRzK<,-ց G#iRre#I.?` 0Ĭv5zGćFuoN2fp4cZ@b)Sie9MOtDbzj[VS0`ZSkg&:\ͶB hL*;}53%\i0w㏏13Ů{ F ?}tՍ^Qص^v)Ibt;렑9N;y? 6M©9jU^HLna0mRJ=OL$(h S@&Qp,@H$9`H$zTe|\ |||}{/bH)&I .|bOR\˕2S+7\8Mݾ'>jcPYBHt k%_΢ii©{pGgCWG}?`bŋ(*v_D!}A2%D$TMT2 Jfk#4K;"xr Rf%D"cs>D"葩T+XJkݶS2k,癊$ >b%E(vwkb^1*;jmb{Jdԙ:/WKX5nZH$)\ͶQod!5×LJ鱭ChU梣SU_ "yfK?R/&RJf*rϦ~y J3{=/su܆mk?$Ԛک7uG\8 H>U㡿 ݞt<1-~VZ4{th IDATw[EZ0HwqXHۉ?>FRxk=_%t9u'6_%8z|QF/͆RsJL*e~=4^7>{=/cV;=C,z]GQWFۀX2>g~E$܃؊B3 mhD`x]ȍ,TǾ~$##z|\ J+BH&D%D,}Whi?=1J6&43^H$D+GD"Ѷj0w K\_qn2aj&m-6算-\ +בK}fKY8ȥ q:8tLRyd!(h#(xXHȿ{=n}C}E;A:qV*liv=)&Z8YB0k)WKijK,SK-,6A)S1fyvn%GT&|DAm% v^e~gdZy_rKHH$ 6~RDK^HLIԙ:k蕖^h Vj MLEn2R5bE$DGI|DgDnY\)SFѥ cRVXEKY~VnCRubd nhԎm^so3lZ˙LNS+ RAeA Լ@Ji&WSYJ*@}??SklzݞsyASWO*&K5 }hz.rbH$zꍝhFIJ^HLIb)SqVSQZ2H$fb _$D_ag˃`&.;,zvsf'g*IDdS0 Aw#~J"bY*䋈ȽAR""r6'ՅS\]ڃֲ,(߾)'mőg_|]|nF '~B~ޡ:n`Stq9zZ=|zoq9LSەCza4Ԏ/߸o]m99$~2c̦ǰ+"e0 Y""+l;4HZi,VDDDք ""l+4@Va8v(/|50O&_NJ-/L;;Nlfe-"p~u.̽tT("sl-=]MDDt8M!֏Qȗ3o@ߺ-.wd9h 5O3\Dd .,䦰'a{[ {ZIcL&ٵn۠"""+E3ED# װ 0:G'JO.{@X"7q;|t7 b7L,h|OPϧ/ ~,䦙J_w8"EɫOUnf؉xhua3r4y;hvPfUXU㰹hwXON;$M%Uch4j!:EDdE%KZ=]񶫈/!C\( ) \C4;nәz$"X[` bif3GDdpt{&fKDDdU/""+m^Z|x-KdC"0M;Qz$"iu,DO\w8" {ZɔDz$") +BG`+Ze|YU*䋈Ȋit<0 Yކ]af2#gȦCe&=LwH"I\!8L, z$"i8m.5`8~VىȦBŶNl(]Mr3g'ߟ 9C4y;:$ sdFlR.$ LȦ0l܇t0FfȪ;ӧ9q_җn8>>>η-~7ӧyWn~<äiկs}ywz*R E8f߾}ݻ;6ÎV(D6w M&WM(ܝFo;-L1z$"TF_d:}U4""QL-wOLm;4UB:fF7/JQV΋0i i'f*=""+(n+4iNH߯jYOmͼTZ wX""+4@d*5Dgh;O[Yq*Siub1n8fرc #󌍍Q(HR~-F_0ƞ*2#HM,?\v7ʳ"v/aO;Wt1T:= ;>BP} {g"u‚ "")N9>plhh~cv#G~6~< 477DDD䣋xhvru$Iy,"l`?\[8Mð;4d äL[uFTYA[»yclzdaUTNDD6H}566w^o߮":ഹhun&Y2wH""@o."WOPjID6V X7""+&m%i\+2$[J;$\VcvvciPbxޥ;,F3;.Z\a,q]-;$M܆t2&j83Di' Q;,Mn: ng*}(Wa(7bp;;Knx'"""w.i̦Gɔz%")M'aO+^&SW%ഩ/"+n:hp7$e4qO FCzvpa5f3t@9VDD6סGrћ{{z{B /-PY&o'AWdqU;,M߸e.GNa&w6d63i=L""+-+H< i<./ z """"7xn#[Jr=v]V&a$~+'\frXDdr}5fM\$SLPKD6!3Hgh5Xj al >gf_$_$""hF:vŹ7O\zwfaEVa:u/s9zdaG_x7;dkz("I\fU<͸z&"ۅ@c:=BOh.Ʋjܲ100MN0n=gjUȕI2icpEz{Rgp;cgf'f'h lwHjVb%GZ\6ii0o """"׍ U\`:}zU ?OPU(}g @D\?O|Y^NϾƵtbSܺbY58#'./:08{iu0Y5biޙysָ]ڶO&u K\d>7Iͪa~JD0H< +BgpMNv7vy U\-PZU 6.Ӱ# }kYFzv0p+ '7e!fհm93}ե"M_@lBy} /C7MӌwEͪRU000 i'Yi-=y8N۽zda)%xcrgz,Ct73 #@ॡkTjeX _;d"FoaO+C3,fHcMKeaQU;kf$~nn,iE `!7#؏ʹc6 Ryޙ)gul>ăݟ׽)"TYFzB;0:ע'y+9L]{ϽNkj9V"YM~F(3"޶{rV~+ 饎χz~lx. a2Fi:*,p_qf6ƶtumժ*,0b!7X"/`6}]KE,JϮs+ àKJQ!_DDDd2 +BoxNq)z_7mVqrGLహmEmfLLf)U 7f3tV3dL`yyx&oGZS^z RriR˲(TrfFye 8s"榿q# aGal2ucwx}+|ۊxw61tr@!o3[ >fȝku-.phvb%9jVD>+IyZ|]40w|_OĶ}fÌ3;K@Vuj ݩBlL*䋈c>g}Kso_as;*xk{\g!7M[װϴo0;Gժ,_h/a% Dd1 W$ ~V~ͪ.ƘN.orɹ׈gW=@ "?)ciB;ڋ]VHF%"v~GӰ{q[< [5}̤I*f.o`.&o~gB(= ;='WNqe''tzJ'zVg<0rlDJoN|\cƒneY+h""uB: ne͎=whVUK\R+ci=Jwh3t0U#jz,bCgp~n3k4">zv(1Jox׆,䧋q 2cD$ sdK[f~Br3,FYpE9iAX~(oۆ7YVZ4|e:< 7 ]oecwLjf'V:Mͪ.n~gu jE>0hu$c2yM5TI N'Kۆ}0 X*GtM-n:fi{5EDDD1i'ڶ\š#"TYޛ崹J ,D "ٔ˖R%/q~5.Gc7mOX,|piM0!Y](W3Lgg(V 5'nNsilfqNf3c*907,"w4ltvuXM3eՖ_av|EbKsor|D 4_a\867= ;iر&'r/ž6dKI&WTȿŽg2L&2FZ~\9EiهTi}i'idɫ4>toV\᝙2`~X%DDala$qnPWB~Z$c&3ΐ+g"p5C Ӱ>=JY8N`?͇;EVAǵiNOpwq?OꬅjBYab3M⟦i[%-F0υ78>ۛ軫)"[J so0GV}YƲj*9sL=XvӉh?]Rv/oWDDV """"dGf2# 2|([X928 ).Esu$jކ]|=cSGsݡA0g(GJ8lk3S1|hE'"UP2ʹ/Fo;XEͽыܻ6o%Mn:l:kbN_xo5|C[5!cgyk1]Nrñs:-f2#yGR˲YU,jԬ[5, \vn݇mۜw46e:=eYl>[?GnWD6KFa>7KC۾g\v蓮_j+9yFxgeƓpG#h#pk5{FO-b%D* |qi-~a7hlYb3389#qWt}!2/"""|l)W0:j}O2Z(f_R8 Z0h<޶Gލn:6}Doxo+'x}Fx߯cϔ\?ř0LVm!t?Kͭ*"7lzƾͥxys<9<oz叉pWjL%qi-!Pu8=Lݿf[c3tt5rj%Nt)۾H۾gaqbo95"R:Cx_v&cjYC6Îvg)䁮O3?ǫ#d|] uۚ$Spa.;3IbM~tEcE!őOw9>}̿Mi}sT >ζ)J'.0;G@Ǔ~f_kfovʳ"axyvp1&oO9YvaoAZQװp L;~B[v"WNq=vx~l)AVaW2aNG`+aOog " """"׉;\}9RY&ʓ㧰͝,*0Ӱa(]&H^e2yX~t)F4L]jy;NB&ʳ"e vc/̘fTt2:6ʕ\iM{+[ 钓t 8ºϱ<+r;Veu&ާX)0H|b%@o'}.ƒ6 ov%*NJ|V\CXr'gH7_PT b328vKu?aO M^,"DQ6JUȇ>xk|2LRc3ɳT2RJ`3] Qqy̬clF@Dn/"r(jی ͡fJ[ns&Oҡ| 4 firoo+W-Y{ ?t;L@4 ʳ"U3[Ϙfrú8l. *5HG D>L\]P`M!yn:1޷\~b @`@Dn/"rhx7ž@CVIMo0jи :?ͭu N\8=$- cq+L=O[zU"7$&=i9v8tucAyVJ9W V_q'ٶ䂍x X, xl1#6JUȇiX8i)ݭAkdAȽ% k8_a䇐@7l~|c6>@e癁VZ.NL$cwxtKrFa:y7|B%;a p7;{& ߱{DvʚSlf*䋈lF [߯w$+"&TPDD:\vk>X ͰAD~& gd]QlVڄEDDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDdQ!_DDDDDDDDDDDDDDd;Y ;MMyVDdu)ϊXO|MjǶ-AQ 'kK=(J=ԫأϺԃԃڲr/Qz%٨:B:B:B:B:B:B:B:B:B:B:bw"""kZ_   .OT*U~/rfLX,d1M`"jbOX\Λ?"B~Je |˲n8f-;H=}P[ ɒ/p\} P,⦶D.rbٗ{[fQ(n\>4MjBsOjFR(bm-K-+wc% ԖټT{3 KR&'y/<οwX<O?v/~)ٹt_{z=c?+)7 y/r9? 7nǿ%R/s_`|rpCl6O wLNo`AZepV~ӟ)µQl3wp:|W>]u_tJ/;+yDɯW^"rwc~ױTUG_c^>V1=3aޝ W)/ݯv*N=㱇}_{mb$\9}G~3^"kC۲@~˲>2t:nh;,W*?x];nX;X\~~b{mSee#7If4܍3P[VDd2Z~#:<7M:{A>/ׇG:T$ҙ,;yK/{m[+-l R4o{5;3KiЉ~ O>z|d2&Q,(o_zex\?}7 qYkwҖ5 -=߻kG8R,R -͍+Ntʵa twuT{'ees|_a [K-#W3P[VDdsS!_DD6l.B,}w,L6K.' bde9vNOW6TO>a7|r\|mgn~%Fǧصc-d syLbdYe9>\2<O!q}7tLFoȳ|7YKwӖ<-+w'y걣kK_<*>|HC畫K9v}Mee󲥷{ר@>3P[VDds""{ↂ۞wur5R63P[VDdsSlz rk|Ͷ}44xI^_tOW"Yk tCϛvߠRg˗Vk/Wn-/v0M~j5w053w'>p`E%ۖ-JLuw.~ <[*ؖjX7l#|nثoyyxn<7Lk~}~isPd۲g܍3P[VDdsS!_DD6=G!-;BA\._V<^,ˢP,t:x#_}z;}JKdtup|Ovy[vK g۹ˆ/:ʛ n_y)LtR,VK^"amfgnZC(ȩ3TKyz~! KK?~+TkU~+\nCl=]dYr}jʝZܨa""rOgNޢP\7L15=Ʌx%*S3 })[*X2wNv c/r:n]_VIg7T*Z{(_|x"@Xaij ʱW`bjZ+o`f.}W3t٠ 3ޞl^{n,K7L6{Yk-wJ2?xҾ/͏ho,wťǎ/E3ݐg ڲ?{wW}sjSiҾ˒-lb=t'esg3wz2L'}{t;;K0f3"־諾?d-ْ,Jwu[xKNJҟW[nW TFz/=yfG?y =`NW IDAT~CL+x{2!w#0n-gIv|W<}£0@ кwۯi~{-8̓_(QzvM_{I͝UzuibFmY 4@e}^糭_,z7 ιZ2$fwv;`+bE͚1MT|JKu˙0uZە*rB9ݙ *-a,CI1?ֶv%%:CG#/T):b,babDi˚g<''Q '6,blD"jjiŰ(#=U-{cYxgbcڰfrݲX,2 CaW;CHD^W/餤ȕB} }:/fiU/]>+z2)v($IvM9Y\#<yϣ$%'\L[0 efr`X,7,Mû)[+$eef L ԅT[(ϧTeeehbYr55(;+Sf*19-mD"JJLTRRD| ªU %8T:}reưxһ%&:4u::8[~J+e^\⢂A_{{.UY 5c4%:CEJ[V^N+SSsLTfF rT;r9plJ?FҖ jmkە2r E"0y}>%I_~ @ w}f =i̜1rϗ]/nOtZۥ<]|EU'aHG$TY]KxM/շRvHbB|^~-IҺwjpOW7+o򏕛52C^~:tdߺLO>Q˵ >{Aמ{rX!E"^}c~TV~YKWy|gq5FV(/_]@V:#? WJ̜+hFIƦׯe|~=hFIFM+W{G ^`ݼ}IKUX7oxwJb14g֌sum~oQGgאxr\^}c˰p^)ܝѓJHp{ҽw!ݦ}ѾO_ :}Vٰn">E"$~ۥ *!!4okCqb(mYI_ vpl-^8O+=DO>_{E"!1f[V<1lk[RYKոf)z&_{K.^j4adZ4M-Z0߼g/gyEPHocwA.IgNշK91x^S {V+3ci/^?u\V/Y0==2 C3GϜsL4Zz#oTRbgO͢|=pg6=#ўv0YăeW~#X(ė$ݮ'\𯬮Rk۲Ӗ/?PǫŅqY5 py6YV}O4\iW~xq}iu}s+-ե{}Ds$)srRI=Ϸ=|5x7VsK/ 7a" CZh/GHt:t&DReYe>$?$=~#.񩩹U%I,Ĕzٟ;TaE" IE-:ȅ3uڇT>,qpڲήnOJJ3ĉkA"0A) শwr$ a=_7RE+󻺻mǒ'~SuE|$(+7'KI=.WVp{_O#'xjlX?Wy\Y#$I驲X y^546GW) ++%YGݭLZbMb۳3zwt$"F- 5mY㈵e 'xNUru8f${~&͛3SwZ9~n۪ofC9aWUy%P~Rzn (hxwNM+*Њ G\WķΞQ  È^XJ եzWa蹧Ssh=r36*uZ*V(Iڵ|~>ώvd$sOɨ󳸙=2}]~0Yhe{9p8~GF,qH۲R磵g 7 0Tu ڱk|t﫲FګnUT( )#=M_{aȟ3s̛Sg?rT K@u n\?bb t݋D::nM/қ[U+5(fz?v;c'(j I]P8z`r艛vMVue}/qf۲m$I/PI6zE[V<؉,qfY Lx[HD[?Kjm:z+ $Ii.ao'Izm2MSK׌iԬƦ损Gvg^w^oBFؿ uI])d,O+ԟ+V/+Ņ?ruJGaڴq}t=ϯ:|ÏOqy ߎ!bіCa1iڲ Ԗ5͢G>`B8v/W*3#}Ð蹧UgWku]Ty=ȃ[-ͦMkJHpD9W/j{z9~J=^IRrR|t,?`|vOd:?{$[8T#ƛiڼ}I3aL㥤H7 dFol&IeQWGU'N.d<=CJNJcM3t"!bF[W{{~ϫ]NgvRJp8aI/U'NfyeZuǪ%q{}52kՖzF^PsK=웪5mYܖS 7wY740t斶_"aQ .':*1ga=7U=IIIZ]~(L{zÏMرmJt:z]w{?bXyÞg,6kxutviZaVc 풤u+%E'Ϝץ*])wTK\/U̹2͛3s{13+ n_'OEO~[pG_}F GSxe9cLDjfY$qiJ$s7LK+%Yy9٪khT]}Mm=6_#3ApXյK0?XյuD"C JSUM݀jzgkjUseHm}jcrs'֡4y$F4tz45DGREU ,nVEWugw$zg1VF-D߾flV}+_ԌQ'4^mY8Dig&n|@ܛ7gR_QYO$=#X,CJ B$q%oێ(7'Kw\.Ijkir:]6';S]@P]JOsTو9fI* *I5 eG c =uA҂yt|cztvu郏vK~p2-m풤iEV#O|LL3eXDTV^1`"R$)7'.~g1VF-OuEIҳO>3GSxe9c,MDf$q//'٬ы,[t͏멬UsK$ /gulni$IZh(֮@ (0J.ߪVhמ C{5=;v/gE猨 /K^w}.fǩVG*VKDoh|]}JIsui^s@kÔmň!b,l[hۇHX\K:r L]і8clM,qvf0f `RkoЎ]iwYKMva͝]oGݮ Iҙses2e ğܜ,N!;u\i* i:|$[&ؿ05h#G77/?/WtBWFX Cp$|L\wޓoimӫolUWw$E/YpDv8&gΩԓ xƣ-y Ne\3 z&Ko ͪ4= CzbCʺf/W2 C0%Iw^|u^y}Ξi3yyQgϫM?Ju%Isg{Ѷٿie% TRKRCc/]CGOSy9Zpnj2t\F~SƦI=Jt:G},]ss%?u3kXe%q\3=Z3A=CBa546%ğ7ghĽL-KV\>఩,[ b1ti?yFE￧߰2SgeL+aHZZtdۇ]/@:uΗ]fw|բ= sѩSysOD}wkJt:$~z[5m8u̶1iڲq8 k3f q>,|\~߫|~w^`9w^KĬCZq+bE͚1M$]ի[MHWqGIGg*keH**̟{V?̉qD[xg`I0 T=.0i.7)4>X2ڲ0b՞duϝ,ڭ /gn4jDUdJ| IDAT#T$bjVie/WW7uQn ªol(;u~>ZY,Z=֬RQa+#=MpX?OJ^]-?G>|*j?sg+;+Czu;Tv;T`铴l9X,D"m7g¡>͝#IWeeX-2 ڦiA9pXYڵ{~qɇLat4-]4O/ݭGNhjbѷEeefhCs^f 딒$Iއ:{V[{=z:_VD֮]+W,$]ҎJ%&&k>ܜAxRT ˯+ 4#zc£t&׮=}Oe ImR;xDY%ժ4IjUZ;:RAk[.UTm$M+WQA?Kuw̌&)P0˗+$SY:CZt$~cS<~cM#O+%9IO|n~˗{JIRgSڣN*;3ߺ]zs6WT)љ ŢyͪSzZnene PCc|{{F:XwjUVj{IٙzQ{֞֡ܜL}_$s@{Q 93 pi;tBB[sVqˠ|BZNI$]T!ۥgI>@ip{r/|9^,ǫM+/'KHO"߽D/Kz /Gyڼu/'}HJOy`{eei%ڹ{ϙ *.*KdV,](I:p5߷]9Y%r3?wUTըYp$]hu[Z{=x䄲2t9!L+ԜY3FL1~_2M0.WTK˗.UfFOpH@58U+Pٙ:p[JOg!=CJOKէ躿kr&$=P0>h9g/\"8}N[ߡH$UPk[*jo~I6Uv]_VCߣ#Owt8tL|SݽJ ͒a{v;g|TpXm߹G֯՞ܪ|9E"ffdwh']KZ{gyw$+3#]Zڽ-^շߢY%ztzUTըNM-:_vI͟ dgEeڰnt|dή3 spد;Ҽ93h͛3S35oLl~o]PX> 'N2!'I|2$^$YHDPX^|u3W jok׹箙W]]#;`ߔd11ƪkUФHn]DQ]}͙ܜ,-^07n]}*jĦ *UIqJ [^VfrZd>itbQ^n23vJpTqQ6=^in?xTɾZh֬MY*V Iy-TA^-^$`0(0̌N0rIQN//=R KϾDAE",W~@W1tź{JIRfz>ع; 9~7Jt&*??O˗.fE DtC~ I α>̌49wA1#jkPYe͞9]mHOpݶY]#*{R"Ȁ[ZTR\Ejn-ƬVm۱[;vNKL޻Ɩ?w62`H$>w\o% 1r+(9)1Nʕ|9 l%Ewg/7j5ޫۣc'fE jhlVa~/Wiٺi,3#]uZpY.;t>{PPXWP MYJRi슮[@ H$"nIKMUWWWiNΙK~y^.jUŋ\!C,ܮ?<<}>:w3rJ-?n**kn >_I+E嗫MoTɕ)x׬[/Ѻw^3/9)Q}Q55?uw>t#Gl|=qZ.W[v|T+?7GYvrx}UVf۾KM͒/!I;{rpޫ@QqQشAkMF=in}nYH>.uw{XH5M{V~nl6nfO)%=gϞ9COQwGtI()& QNN5K^^M+S{.4eZ5wl()))fSFFƸ~SgKo| ?}=n_˗,gtkl}s$IfϖoR$QgWv>0 /ֶvH@@Pm].QkyڵZ$sIZp: /)( E;ѩHTVf,aF{Cm/_aHחzL6[O?_ߢ?fB}OO^Eʳ奿0dXdr2[~zF&Fn]X+_$Z0oV\!I:z޶C *ӏ?]M_H۠!i}D ?;02uuvFy_S[d͘^"<'{j9*.. e& PRJrsr;,|\~߫|~w^`9w^KĬCZ|W@@>_Avϐ$//WJ֞SCoDgBtM Lӌ&ַS)IJj^v]ћ^E4M/+ө %I!%''rnV+=-d媮N^oI;wJg֍>)K#\e^3ͼ+e QO|5uIN%%:t&\\X,JgZFIZa(/'G jhhP4eRwW>_6-ėiK5|^"WڷNS999Ύ$$YQ%Wߐt#4m62ӮnZIi7Ȑsݚ;g PSK~BBOwPrb@.K?p33c] D>b"33C%bYV(c6RRNG[~XW8"X,**,TFzjU^~I]]B2 C2,kZY3KqGDF"1ӕv㑧g(%:VNVv{l+ cD>b0 nnO@LYb]0I/L6s.1زźe ÐadzX#J;$`{KmJJJTGgWkJU˥Twʸ&%,-1|D>L"Rg:=**Uá Fio )Օ2egL3XW0LT[{sLHI0 9q+8 `*U=H$cpĺ*0.%%&[yYSxXЃD>L"i4MutvCa::1p,gL5cgH$a&I%Sach"2H$…USx!DXC, /$#$#$#$#XW蒪wK$-R=9|z: oI;]+=+>ZJkTArĶ`Rch}z5N$u\B^ɌB^tydk &9z` au[T~^GWO}ȧ`įtU#ۥwVlRFR,!:ZC*( (7P(!Co:p;Lrd 8ߔrlDvC+6(+@a3dL0QLjHP]:\ZuHkVV٭ !{] oꐵa͛J,TDmgt]XvC+ )#)_&V2<r)%XU%&ㅫIߦcj+U5W-Ik6֞bI9R/}*ul$9O_)碯 dKL>ݡcu;e:,sF;"u^V#M.g`rzG DE0u,cP|aD> *'ةfOj:&j~I6I\ӌ;!_[I{Z5Zu*I_o).y+!Cw+m7#!SiR)iا E隙Lo2dhy:VS4.ylTraTVo$\A6.բ;GeZO|!e$iaޝro>=R*٤:Qv\|EN[fg-ẞ@e/T#UGVnIPYmk:iZ1S:|ZFRu2%m/{Qkaujgo,080Et ELe$iVrZWd,+ϫ7$o|nآ}o+ՙECyumzN:..?fͰ>Z(K9|깖?T^al&taఎvU9mmy\=춶J)ߡXum=[[vPu`0LeTWlRuw~&ݣe7 H"%2L6<#ʩ[%3(# un3l7h[Hu^==OY7 zuW>zVՁZOɥ~^=8j.2>vNTX+J6je;&'TCQJe\K\_i|vtmHĐ* Q_+J6*.Oa׊MW:|RZUmZ {_*r|ژMŞJm}\\B;_ѻFөUe,FD]d69mik76iWW5 +|Eo_XT꫙ X::^TI<\W+J7isa:TQx*ժ[=eQu[UUS&i]\r=j,^lY}'_?+#ZSC6,; !*ߦXZOD۪ 6 NS$1^Q*ԦT6Ӯ2ovP"zyM+K70K|6.;&ͭeE-5ej9C/i4/˲>t.;^օCr؜\W[kWNݑVyKTxZ5D;tm♨ղFas}ZQY\WOͫ?;F?uOꡆgXsC|I2 CUF)ߡXct)OO! D;TWKy>܍ʹ_ݍGͥ[5_m䌒ؔVXn*ۦ=+ͫsTK X IDATL.Sx[PGk+T3[M˵CJL1RuU괼x/tsm|v7E4xTݑVWjWW U*n\vUKN%2Q}W}e'Xds Ǻu]IҶ}*Vq aSMpZ֨owZG)ML`iwkk>M$tu}GnOvI]9_T4= {1{*%zͭUu}.։޷T)Wmp}vuVlVmYq֖ǔLtn#굱jVh"OVoWgzƯH׫zw{d{-I1]:X˵!9m)^Q{VKw;d3j,^+5bȐѮ5:<֔ >Kw>lA.,ݬT歑y :zv9] )*\PRZ~gŁÊ'nkɥ +*}06Ӧ5ik>ez^tT]˺:|J\Vf5mq  ԗWlrN;Ǻ`A)TjS^^5mw9 ڊ]2 32T_&#$HG_Sw.#@fUXoi仄]Ejإ޻oS:d{}gf  3R_6‰A z]yGƐLUJd>z.%w RD>v^|@^ JTXt6U%ӱ|@^e/S2WYYV.e7 |ΐBJe:rR9,a ΐC&umrLK o@޹^qN j0ڥ,a>`"yg6E P&RYer|@^XN9+J(|P\vBdvuGZMEr. yG tSbObǯ+M,A>(NGM%dU2wI;|P06VlajxfBW`Misk ڡHbD\&e0@0 Cn_2 "O,A>((L(,ͩαK". yE iZYYөeEaY] 0UQRl\]'] T}j9l._xr$%0o@Aj,^'ݫH"%Ğ.,#XJgSr؜. y7ަ񫊥尹Tײy|̩_>дaj=s[+\#)*QVn F4U2e!.h:"ͭJwj[.S,`>dP]h.]~%;Ҫݯ9嬬$)K}F JRCc{`S~YV.%!. QDdeMΗT VUoPU6Dy"rRJgSRoS<=q\FmR< s ,ͥJ#h8֫A~,K\JY+3s,YYer%3q5HO#>ݱan:g 0V\+5`[uU. wrº>zNm,ke"i$֫HrD2,2e  ]0ZQa J]2|\CnYVNa,|Fy}R:7V˒lCErjUUFnOǻPerɳLT]/5}S#}X A>(hAWJ<ղ~z:etDh$ޯD5 Jtarٽ*vW[LSaie6*j4> ï[n|Q_)|P TeA!O /ӕ:z"ה&mMS3Ѝtf:d3l2_v?MU*K4lr;9B2 ,_A>(xe*vW;ҪvTwIKV)ґ_1 źUeϨt|5rܳ>issfo(4UՑ(wYKN<=kgtu/*i]CZ[K Eks:D2><<۾nko<<}F @sٽ*V(`S E-.kvǺ(x jUX|@ݑd<98pXmgΥ|ZwlXl]ŞJzkKEBl.D&zc}bD}#rrN6D+Khc#7YR?8z&޺iʓsU A>X0=/š# _Ԇ.)ل‰!um:.*ee6RuU d3lS0LUTh+܊ ,ErUt]u(͜ڟ3 LLÜ*'gͥeYmZwMlB6!ͥ@VmUs䴹* ,nO%*yA ƺk3 ÐPKyL`R81 OG)LTm#g5V:$9mnWjM.ۦr4 |ܥ :ڔjC޿Z[vWzZϦl.h>zE*MkE,KY++ͫuZUMˊתS%ʹFO|I!wٔ}NCG\M*(5*M7ҦoubR٤WRv1~n}v}U"#ckE&Ն|e;~MS{OΪhVigY</S5]v^+|Yc~0TQPuI%j9nb+{HeA`4d&'i},α~c}MKYE0{U\2o*]EFNc좆*;. _rOOJׇ╒B 5wXz\m#g6zZV z9toPmh}urswu3Wβ$I٬*n5W-Ey1h8R,w)@VbOer~DL1gͻ4^~Y={,)c4Sg :[bOziI :5d%)PLj<)A>,IUw$RSln:e|&,2LE:2J޻+R&R4V6W"(2Y j8nSu}4=&!esII|X"ҹƓY ܕY,2R]v9rɐSAWlcYK$-f ÐidaBnGV.d˙j0`ǶR7:.];) rYmڕjxDmJ\ۍi Jyet\d_XѤn0@]T\w gDx**dF&EڥtL2 ᗊM;ݝJUAmvi,V&i;Z^+5ӝ2`H+$䯑4xZ27mTJ1&.Ruc&sڵ2GKUuj(E,"t2=,ߥ3]A> B@!P@( | A> bwq-%``ZryKuKP@( | A> B@!P@.484h,ҒbGǕd{qo;/pnWVTJ^g^ C:Q.X6S*mZtv^g}>|qY)I+kd4yPO??3e۔NgTVZO k%Io񵧵i}d.^яK_Px<|B?;&oĩ?&Iǿ?v_ $pY?/<64 qIR.g׮[JcO~Tz,Dȸy?w1}x$Ke0 IRF}w9+}630^|u=svm"Ӽvzߥbjͪz #i,% 2M󶏙X^4Ș6[3s&Qq9 Y4Ӻ5jчI"q=qF'0}Gc1]:104< [Ѱ$)nKW)+2ՙsTSU+D,BXL^G>ϵw.ZVTeE0y&9.OvkeSB:x>oKh-jY =ϧ⢐NƆ{۹mvmrcE.^'_Qq(4b,KdBNSwN/jY}k%ING_ẇ>>>:D2)I #ַV>wQlV]=}Qm2ecJҲ,tH6kњU+o6.UlV>2̌,l, 6(W /.ݮL&=ns_yݽKcG0 Ylݻz,4O&8? m6Swq[é-?՟ 5޲~}6|u:mݴNȸbBA].I/su8蹧h8PHFqw`@Os|k>l;  `  3:ᰫt+( | A> bw|QdRQIRYiɴS.ֹXLihxDeZ$}߱^ϴdݫr]jjWUE Øv__tx"_J}gy IDAT/lS:S$=C:cSO~hlP0ͯ]=w2 ? h]:{Gⷽ^[]޷M},mlX2~6OW_4q_h"euZȸ_(q /.Iھujkf4. .@X\ih?~?kE''Jeٔf|^wOG\Βf*Mkcj"U]Mߖ$ھEՙu\.9_D2ߧ?(tcepH`E>`Ac:~~J^| YM[|Y5 kphDeJȯ[yuv7E"%/XXXNNOkK}˲KK<ܮ{á}z>%)\c/_ե+d$I:y漞׊$׫?6oz7u69_"Pr9K$ˏ?"PI:}UQVmI}\N/$鑇HZsEcqTWBXLzԄ '?O$lXSK+KaH\Օm'?Iah{ K/ͭ_lUd|Bzx3վ%I/xk^{3pqO$ [q@(x#7z˒oY51`iaUfKl6MZjC%N~RRYQzIү~._meYd2z:q$iǶMw֟*/XZXީSg/0 =s`ǟ/o~.]e̹ǰ}Ͼ}?* (L)LJV77vϨo/XzX`Y^xuI҃۷6Si:uΜ(4uڲq|9PRѲZ+LfiuǴoڼafG msvO<[;ج^j (,PP;rPW#cnqD< Z9e`y  ˲],t!I Y ˲ %}>8WR{!  0&ŕL\̏d*X<.F4a>,55Y ܎ 0dB9u^ &eY OHE 0`)RS|}AOe:vJCwIXbu,5U JAo^¥cܜ0ydY"QeA>,R 5jeYZbm+I`!,_,v,>EнB. `Y>ߘ0`1*R .WB%٭!|> SB,RG̭+f L1.,,`"} S|M7.X,f_| A> B@!P@( | A> Bs̲|0 (dd a2 #eL"(|0G ÐeY,K6T̡3ښ+ /≔5M_.;0X[!Y><~1%fj^^ B ɲ -j,sU`E/<0n/N '`nܚEܚQ 0ۂ{~Qtkam QXs쳫o} ` Yp" !>2 B@!P@( | A> B@!P@( | A>gãz/HBHf 'c{7e-f7d7qĎ n)U4!@ LyA.hb?s{瞹3s.A>n</ӧ b tA>/ā:(--.5Fp#|A>n 7B!Fp#|A>n 7B!Fp#|A>n 7B!Fp#|A>n N@w`WQa%_Zhp EF}[!Щw>TuMwߐ`eo|jjt9/kg9NM;JOGi޹>^^'N,>lC6q8Zx+}󔝙.ѠVݸMv93ծmFɎ=tT SrOO͛3]uڹr!өGOjI$Ia=L~ߴmf$iD0p 5Z4~WV9pXKDe umw::x丮AvCa!̹ :u  є c 1td9Y4Cє t:Up$nwݥ+uh[_[W[w|Yguۿ^Թ‹jll$?/.ҺMҢ+J;k]uv_JGTSS[Zc~/Bp8{VU^:}B|zZY#e4| UdD Ziyy99u O/>D=<==\›_a+BaukÖ6T)ɉFd[U]%m߽_͟*_)934al^Ozň| 鵎lk)7+]5x_}p:?~JqѮ_F ϖ$9v}kemd2SW?7| FBTU]kFk[g[m/-WjjjSNAA VYE6e]@U_+o=#$*"L%eohqGu- Il+h[s?ik;lvld4$8ư?~[͟Ҳ [:sBm64nZ[mZakнYk7mEY}s=,$t >TXhN+t /U1Q}I26M~BvCA҆s\ۣ"4mXm޾[$9kjA~ְT=2W+nҁ\~{}˯ZWoPkMg M?@ 7`0t\abayOc]uYKt5nQ55 W6]̩4eU*(_=jSMm,NSo;T_ îOj*--U@@F +Z$NƎ+!ժ_J:..bP֡sGsg2gEQΛg-fܴNXh-g0nnoL&?Sx*vaD>)٤_e4n^_f@wSFp#|A>n 7B!Fp#|A>n 7B!Fp#|A>n 7B!Fp#@wht`X n ^JJ@wtSFp#|A>n 7B!Fp#|A>n 7B!Fp#|A>n 0 ._՟+n<mߵ_/_Quu,JJLиQ=U\E9_**A1Qg@p+޻:JaD> +W7Q씇٬ {h/|i~Zoc 1"n;ݮH&I?S !5VV<=>2v[>NS֦&Mfyzz8VU2 65E=nͮur:oh"{jZ#ԯc5n׷ ߢUU}PLf]{JWW:|>2=ÝB|I~>WXRYyFR;'I:|􄖭\}M?&Ouu:v@6]~?[9Y钤F7TZ^!)I28^Ck!ì,7+~*8s^d64m8͙96 )2"\7Oqb\}rke4vs}>H4aH5k6nݩMvY^^:ifL'NѲkUQY-I/#ݸMN'?xUw-:z@?I]BOUdi5yۡZzeg+$(@OEɉ shރZi$)=%I{<)ќS`J6lvAɉJ鸧ݥ+TU]ai?g]+'t\ZZZiit^ ?5jXzjub4{$&ߞ7 nWJaV!)-П_y[ q1;T^Y[w[2 SCCoSGh-4XO~!x{k톭z:tdt\w1U>ֵr;"Cu\AI)I3c7KfI{ѺM54)QIlhlGk%sd2+zFզ-;ZY*3=UI$I֚ [5fdr3u1ݸMFA3NFYEkgVFl654wڥ\ qZpL9N;V IDATS\lu=_T]|E[wUdxƍ.ݮ{la{I%:QpFL$$0+CE5m8IRye$I5uc4Zw|},e`f6Zzo>"ôQ}jMM:qr3us@sfM} ۬\!AA}NqN ӏ?⚆뫷[S*#m$`0(!.V{*"f_]+-ΕR t/ԡ2ڶ] PCCk=0m{ccuilTiD񓧵`k%î斖^ $״%QU]$u*O:DNZa_\`2@HJwRgyB\OghQZi23RkA+%9SzהWhHB+Ŀ.kXܫں:؎<==yv>l  Um]{t:e2h0J2pdtLnO2$gq+o/WS'Y2M'`Pmgvu}mz}{`6E+TV׺ڤWJ:ݷ~ı*Tڸu̜ҭ럟ݶݪ ].VSs@2Mm|m.wZ'*"\.\#鳅r:t9YpV!Zxϝy9Wё:o^WP 4$Hf:}Sy2ͮnEem}Qss|C>Wgƻl6+/7UGǏ[%I֎_4OnrU۴];v~)ƅo]NuQۨ2Zb+et(v=cG.*t8.S pEGEhQھk]BETSSKڴuj5mX9-ZDE`V٨pe<%(05jm#ҧM'AzU]]>Q:YpVB/h4jڤq:QpV+VokZf:iƺ‚[,ɠH /S;N-z{oѯ,2R닗f;O>#%iܨl=Ɨ$//O-N-x~C"]TXI_Zںzշu\WJhRW腗ߒ='ՕRw/Y\cFh$I;{ݸM?fTQYE*Y|s [WŢ qJL'[/.$ GXЯ0e]*֢K]eymDGFђ?Ѣ,2{Iݟ^$&o?tTCk^Ѩ٪oG&Ӎ>nV?(F=VYM  tc4~L^}~ݧ;Wf6 <;w\=ge.a룜7 I`w ؛x3gvnf"B?s _@q Wڸu$ ׿:)8{^ko}J3A> 9"[#GdY'5y6_{e'|A>n 7B1tӮ)))SN9SN)r)|t:`0:hq1nssus:ìVs蚲S蠸X22 ڻw kzt:;vV)$$GcOC[rG},.ϝʜ]zG9o3>n 7B!Fp#|A>n 7Blۮ}zÕvk7i-v/@nhhl..*;WxQдI8ݹ-jjjuTݕۦb*{yKWe04(&SE hกVg]К [e4@wsq%H55jޜ nw0_{$ dW o_A> k6bF;o@wضk[<닗(+#[!6&RS&ko}/Ej+oJR]}S+Wi/І-;QYǗ^W}*R7]&2LzWӉԡIw,wH~fF͞Jr8$ɬȈ0Q!5 Ha%]TtXgLOzv]V|}|d2:mkmmf锵IfYFe4zpA~~jV$?^V>Jfs}nmVr){yyz|'Niރz+4$X;mm|tTFKrj) $͜:Qr?IRNf#pJϿH_{n)}6А V]].\@sfNє c`lߵOudԉ7~Tsj23R4ohԟ^zCr;28^Ck!ì,7tݡui=jimϝ1#s%[A1Tג@?~[eZrΞ .o//}[ZШth4jPl71^|m Μ$&M4NsfN /]FGO^Ax^egMN>%VeUF ӳٵbFX--Yz:tH/[JF$i=w* ($i}>#u׬iڦO9Y|}4sDM?CjU*8sNN-[Q=tjӶڴuVIҬ4{dedY9{*y`Ӽ]BOUIF) HbcuԒ?qLIJ[~Kmtn*Β*,$é9^0<VFP=p\UTV녗ߔ35<;_]xY6mWYyB4}NpN>ڹm!ɨ|IWTiK௺ziԉ5k$M0F+oPXhΞ/RVFL&TzP}{Pn.IJOI҃ޥc'OpJcr\7SU]e+ZY&w%IAz|~^~]]zMFtϼHVDYۿC$i;Oh'땝?%'&hSʪjopl) /&?{[WHIҴIc}h4hPlTyBul9o 5FG)2"̵n4(O/OFK2҆jxLW``ߓ"PW_z({Xv9)F`h\VQ}FUFQWJ酗Rܠ}-;()W-jmu:fsKl2ͦkeHUeU𱮕 JJ׈LIҦme=tYY#gi\FyzztSMmϝPmزCyu~ݧ%JՂfnwh՚MZv6Q-FY\Z'[(IJI|I[|uToLk7nwئvnv^|udCy\[JzJF+@7ѲkUUUݯΞ/ҋaΙ:SsgJ$IyY28^Z5(&Z3NO6o>3 fV1m"lZM0FO~A^G]YX}Oշ>+%9<[WKJu͙zU¢˚6i\?3uy]R4IbfUt񲂃$%0EGEh<=#ڰek{|\"B3&K/AQ(!.V7ݶynh3cG*-%I6}8v&dv|lܺS(=5Yƍ93tJN>k[A:XDiܙ։S%Ig=|< Vj1uJ*tnp8ZyR}?Hb3u͙G+-%IO= 6l٩(&*Rqњ5c$ihR(-%IFdl}BC4(&JJOMVvf}е͉͞3*#,PZJbc4}x|00kTrRV^)I2Lt`PDxhmA ߲'+nR$==sgjx{wSQu ?B0H9Ym#Ldի=@7('+Cbó4(6Z1Q|<<̚5c:_xQp8tq͙9l fjp Y,$Y,5m=!.ItM͚:ql lnF}4q(M8V~k*-p}HKIRzj>5"5VDG.[b^Pa⫚4m%ȗUU|.meÈ|͜N8 _+(0@{VJrՒ2 Mߧ;(0@ hm$UVըENօˮ:F`CC~譫3LV7InjШ9='+%7.8~WUudw"I32Wկ~gք#]kՒR54ZWvZ5r8fSbͧн+%)H(m[FOݶ6ѓWI>ZU6|Uu;ܲZMJoyQk}qe/OO +zr_Of_``p8e4TV^;J5NYV ]͵rn_^}wzՒk4YѪM $ozY$~$yzzY,8~mڮ̌TwPa!7Ewh>elتSƻGRb,7. PB\v3gdϟ_լe2t1Xv5ܙZq/Ydyzz($8PNCN(6&RҴxrd+,4Im]o^ |_*w5ZuO1iodtG:$^v{(H%ݘ&6+3M!}k6r4dplߣVӦmgw OiK4vpdeUWJ\uCCO?8Mvk{6.?2Ӻ1б=,NL]˜psy+&d7RtdD}{~t`Ph4 ֩?}zbjcϙTI"k\yzzhރܴP}C+P»hV =f7t8FO~,*,Q#]e]gj;vU~#M+ImܺK=xz15cXPUUhܨ=֑֭c'4ǿGѥb.fZtJ VFP߼]fY#s)?HƍT^n<==4"'S[vjg),$DwيTSSΞQy9>^WWJTS[vj6w0_AFt9 cG|l>ں^C?WV} dRKs&xyyma!7f~_Q͘2Ǻ9N]|EqQg骳f.Ԥ5nϯi֝~{g!Anv>"uN?>[(ө$yyzXa θiАBa%e |QCtdNlV^tJvx'uM7|U,F IDAT]]=O^Bne!U:}Pq1.]aeԈmںKaL׹"۴]ҍ Pqf`0j';q8r8r8.r:ʌ :pV |}}4yhJQ~뚚*áFk^ Ůfkp$fehzeZpNu:WX~~_.;hnnћ~Gnkۮ}2To_٬ <>{^F(00U'<&ȱ *{X:vӏ҆jMZ~Ff 뾭lҤu^-_A9gkeHMCto#ܬ=khRJʕ("YIHVMM28[}ɤFk뎽 RFjNIɊЪڪP۴]vM&i_>OG'sPӧLG-j#=5YёZa&fgf4 kpZͽc/B5ih-ZDJOIVJrVߢ@fR;3W}}^Z/?f͘oPnVN=_L&M4N' 꿞~a·r:zib=ϯ<KoCC_?-RˏAdf%&))1^4np۴]sfNT稴B]~JjwoֻKWHj_[ؘ;ukރh4ɯ?>^k7iŚڦK*tHWpwu=]d} hޜohp:LpPyQ5Z?{ŕ]ݭ,rDH@9`8g{ƞzwg~l=qlr&`L9 @YV DdzzԩSBwѷ9GN? x엞ܿ|;(/"-1=矜h3Qu5DDD8nd[WADDDDDDD䎸3%@H;@H;@H;@H;bk |t+]JWzt+]Jo"""""et0j62Z=Y[-mmO7t8jW蓓Z X, `Ν4M|8bcc <䩕S+`d[6k&nSk@iGiGiGiGiGiGiGiGiGiG*nj-ڴms?_f_j=+lhj\0 Rں*wsVZZ˅tzi8u._vqto EDaWuv&88641&ITd)={|zi.8?+]|\m&88Vpi =cq-EDDDDDDDNP _D̞9ئ[=W\x=^ZUv˫v7MJ"#"0 b&Fpp?Ahh(Kr].7^ў^/Fh 4r8 fp:qU x,4)-+'"< [TTVMӤAdD-]]UAAl3xq\M>ݿ@  &4$<\;zv]QnUUDp{ce6,Ǎkp""jD Jٱ{?6ne13D6lΐA}ZQ|^5Y 䋈C|rc¸>_ҳ6̚6?!q1<<>-[=p8 ׋gߏgnVoچYMhHcG1~P@M?@tʠkLGyyAA6JJt2?iBjJO̾Ԕ$uKWS^^fgn6gM%,ؾ{PoҸ:ײ W^4M, Ry`$:??ָQØ%׿ :*8y,.XkEX,r=s*Fj:z_| yK_}i~voF{FqєWT9vGb|͜B.VϮ=*Qs @z4zd7ضs/-fYy/4y(*.9}<>IjJkTdD8(Ζ{2qѤ&'x<۸SZVFdD{qEF_r=r6y<1_<͒|4f7Q _D\V ٱ33ٵ c4:gc1,=~/--|% HKMfIX,5-ؼk72tP?Yn`-kL8^=r(*.aμE\)ƬO,^ ; y*/_ƫbPZVNee%,!!\xy r&f7n0"o @rULKn>{-?ϫDB|,ǏfIجVv7-3Y38|wㅧCZ E%Fm s7y9zKW#6ΔcXf[vaƔduΤuXӏ=FкuGdvLVT̺xO鏞ozc/59S'`Zܿش嫿`P ai\Jʙ>eEVܓڅ DEsr!dmx9` O=BTdǎb&煮HkD5|0KV6orN:ä{Fѻg'Na͆OcXxxToqNfMDll `}'Μaıx}>|ɋ,$"""""""">EDx9O7n=**d:0}B9|4J<{H>!t푤&'Drb>ɚ/6=;B̎(av:۱C*Ə[Vg2b>8>q1m{NW? tm;E< r萖=c_Sƍ܅NWT\®8nC#<,>{9sLDx8Vqѻg.i) 2CGZvd>`XVN۵7z""""""""7j/"F_$V׵AMgnZ=sٵ׮ˁCG=UP]"7;Nzn.<|E%$&5[@bTVUIGQҕ,\ڿqߨW)->.fG)/JБ|v=@qI)!!!x}^].}_2#K r?Ҳ2|>Eťx<:g6o ё`ZNFͶngu5s6(ҕuO K\-枱@nv]{h;Ng5>bpZ+x}&xRx 3,"r1}>_Y& ^w:;e|''%Q8+WSU?_NY!i#!8jڙ0~yqY^|V~K7"o.[jż{nWl^`Я7ɁX&11\>p(}4ۻnyb=Yi;3ۻ{<^/ ƍ˚x)LZ^h^T{}c6 -K3`X 7_t]^pzXGppvS`>**hr?շww6o]i7?y~{/].EzZJEDDDDDDDR _D[f1tp?v;b!.6N斈!( O?v6ߪnHE>=ݸg4M]H)9v8fϜϳrƺ f! 2㻬ݸ7-91#N0_/BC\!8(eЁ-;NWD^/Ƭi")9)1,VLXX(ݺtBExO<>i_z1Mׇja|<{{k׋Ӣ-L8fX,5dZ9t4qa޽1{v&!>SQQAn=tԱ$'r .\"88IFq1؂llںE< ׋8pݲ:A.\bCYE>IhhPƎʗO0d`_2()-#""X, ʵt2 OddUG眚-3WŸy;pgؑ\^uHIJGNn[sjkZ1[vC&8Nt%59evIJgxƍk.wRlLM+m;rϘDr[UFtHMfڍ8N +|xS&sf˟wc'N-سˤ&7?g=,Z?1AAT\t‹^0_/*p|V o ;K|6ѧ;~4բbۍ2-an6nM۱Z-6FRy-eZyǙR.[Â5cӪ@{Fk5cvʋO?0 ,4~ȡ=_{s>7?Ȧm3Ә>eB2:v'g|,GNB|M[پk}zvx=rѧg.s?_ @ppsOХS=4ї2Zsj7y0M\ǂ%=瞜 O?̜y%DEF鐖l]N/6u^zkƻX,[ֳd:lDD5W<}KWVڅϜv/.׽ybGn O6(wEn, IDATÇ02o0%XV"#XZ򠈈F&F#sKړ%rZk^VkV+t3OO]" }r2ۺ㥨l67zlCqi)ᄇReX,c7=Y]2q8DpR^YIpPQ1ՔWc3.|snpm򷆬4MK1 wBxXˮgSׯ9>IQq 6h{mUNg|x(-+ ""<-v{(.)%4$,SYy;R_ov;0عs'a'?7M<|>/14yjJX51ZifZ?l&P""f%)!MdcGۣZ/,4Ʒx{hHHݦNDxx h !>fk-~Y,7Zdو \ (/""""""""""M{ VF""""""""""""Z䋈wZTd$m] S|vD|vD|vD|v/ggg+]JWҕ6Lo+]JWҕ~7DDDDD0M a^mdn^{X[4Yk/jjןnp8~բ!"9g''!""""""""""""`۱X,Ν;1 ?ip88xߍyS+V/ɬlJ3MHz9f3zu/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""""""""""""""Ҏ(/""*U~cN:UθPp򊊀+*p:q;ko~pmUYfmhV_?x$?VMDDDDDDDDDD[[W@DJ5GYyim'1!k7n7ΜCZ2t ܿ kR\Zy}K?~K/aI/v3.Rĩ$VOsW\셂yO9ǶvPzvcЁq[uS[b ǐ}۸F""""""""r""߰Kٸu'{B{%+qQ1.\.ᕗȗoބ#{ ]w^}zp\-^c!m]5x |9i6fϾR[\ިC())eν^v'*""""""""I|62cbkZW\,]mouIEe%w5l^4r8Ym8^IxX8흋Yb!88<U`Y[T`[ApP6[ϯ- r#G-f9*U"***lp>Uݿ-a&U-3TVU/TTVar5Fk>6dÆ `_ǎR _DnYrb?4?}f7$KWQ\R x LOK!=-cO5{嘦#.DDDDDDDDD|v!$8Y_LmMp: a<Ə i&_wO|ѧg.ù xSRZƧ qILRSxb$Q_}K|5ݰ?l\^]f_:ƫ?!:̂)/f37ٳxo}q\_~q(~*I<4:;VPT\bߞ<8c2!קΜge\)igt2^+bSGE<7)/cx<^"#"x`]so+*xPxeeNtɶ](/ (~y`X57ޝlVƍ cص2,ĄF?u_Qؾ{#qߤq@͋oŐτcMlؼMXh(ӧg~˃ߴ0oOxx?GDxƍXo5n.*GD߬V+ӰRHtHKnDrxbΜ@iY9i rQklߵSRZFEtTSy|S0 ~t8X|-R\.7g0y$1!gSI+-+W^~А.\̼Xv3u6e<)I55e O>=q8,\?1V˅Wy냹Ĉ=~+mgı Qx:2xY$WǷ?"cOȵbm[O<QBB1V@N#jF>n] p5NK90g"^c{IN'i/_KV 9m6{dtLVeVVU56/\1_/FaÖ\r{PrvZt@zuaD@va8Ϻ|xFLZjǎù ߧ'7DiY9KV'/?Crb%e|B[n7|#c$11xbLӼDDDDDDDD]DDD|\,Wb3ݳx)tȃOGn7no_[ܮ\t*O&4$'O=+ǎ<4;ӵ ǎu.\h=SIJ'cFe$'%0_/MBVL:g >dDNL02.])MIc $12oY01o#݃v[1 3=+ f\x'[JR"Ǎn")1tʽGx8{αHNJ{NWa@>=HKMf.\B>'o#qߤqQTWՉ>=sٴu;_iG*DFFP\RBu:yruc\/*!1!ζ]0 з'G Yt5e˅Ηn^d uqI)Mt~'Ų;RoEr!UGBCB9mo+׮xm-""""""""8EDڑ3.`)I5 7o^[xx]:uБ|2S)*.e~صjZ9#PM:@ܮl_$ כ䖏 nXx86onK%2"޽r pzD#m&F^uZܣ]ΛfsMZ7sQh>yӷwn7/7Z46aPV^>U[gyҒw\bk<ޚ7ԙs[g]:M= {|X=crU9Ε«<̻QDDDDDDDDi# .Q^^AQIg]`=$%3s$QXa+KVe`CG7jXM}z╬۸GXa+`2}MB|,Wp{[vxZ%22KW#+QYҕY@e󐖚 (--#:N\.7'N%sF:]^ħ 1~vnQ=DDsۘ&c^|tch9[f2|@gWR^Q cRx7~i֝8=` cz=9`ߍm6˟sO8 {hF/S-cF`l٘$~c_m"#ڬMy&37/?^{9]yԧLalٶ/6m ,4(pUzޚyD%air.XynY׿M׮sYvQ _Dn[|ݲ:#KJGE{h?OrR/,Gn7)aaZh/3#WC**p:D Q"""""""""g4,QiRkd nh֚_ZJ?p4.N?{>9m]j|fz<^JJG~KK '<:b&%eADx8AAO|KIi9 XTYU$EPPP+*p:]D7E%MJj[{&**r8n랷FiY9%`9iR\Rb!**>KiYA5t@sC-Z{T-EDDDDDDDDD仨ݎb0 v܉a M$//χ 66! nMZy<}}MLfeVYoF:1 ԫH m~l6+I w65-oa}jTjsGnTd$\bowRXX(aa]NdDm[+-1 Ej{`sAƂ""""""""""jl"""""""""""""""$EDDDDDDDDDDDDDDEDDDDDDDDDDDDDDEDDDDDDDDDDDDDDEDDDDDDDDDDDDDDEDDDDDDDDDDDDDDEDDDDDDDDDDDDDD[[W@D&??߿t+]JW0t+]JW}o/4ͦ3Q{z`Ro9d5ln^U|䟽B̶RPPnb`;w0 ip[|7&O۪}sH>z;R7}̉Sgں*Z~8=6nu߅""""""""""w""}4]i ܟnYZoS\ uͱ㧨2\˷]r9ٗ.q{|JK8rDo._oCU9^K;"4$JZU&ITdmqY:+/=sjUCG Or\x7ޝ+/?Kdĭ]̧w\Rڼ.zwY۴mR~Kӓڴgap߽x9Dm]%VGSE+8~4C1۸""""""""r'EH;7?=g7H7Mʪr&zu&3#*^+*x9yp:)--3[Ta&%eݞ*Ӽ:ո\0kd+m4r8qSQYE#p|>١n7[7A_uzutֹF;2Q y^+*~VMݿ|@Ͱo<ǐA}ma;z """"""""wZ䋈|VGT'Cx?oH\l Ϻر{?.aو4M.\buҿ:|KWQ\Rb`ߣ'hE%X,Ƀ3&\S㗿=ݳxx9;~ ʸQØİtLO+1+0 E^y.@4dƭߴ АƎcXn%uۣR*$'%ْkZ_yE.XΑc'|$)t N(v50 IDAT4q[\ 7m+ ۓ' VĆ;p݄2}x//|&)IfっƞܿEVsN'78FIi(o^oԙ[+HIJ՟wjF~pq,]?2n;˃GXdL`e 5{'O˸zBnvgNjɵo\Ml#'; [vW@Zj2Ob QdD8O?6ζ;y_ ]BFzO=:Ԕ$o^o ^/ aaT9X,&˸2**+>jaWٱK]/?x0?1NZQ16n??zh\MksIWPQYEdDx/b>%+jp9'Na=3'ϰfV|c~X,<xg]w0eX^|a>ko}EbcSٻ;3 Ht؀+q&f7{7[rwfM`;l){$@TFmfHcD|ms@Hgص0iXc9y:6׹0ji ǞDF\V2Μu-}~b `Rt.fsfА`,N~{ٵWҷwRb<=l;J~~$u'$8kVV8/O<<=)dˎLw۠o˲4_{$% S`Ru6:wyf8(_$yXS];53 \3`tU&o..ec*.)t!R˕8yiI,c Nrvkɠ@~ L&=LMܹګG+(7瘶 5ȽlmZ^z$){cXS];4[ח`ŚM<:g+pMDhH0E LJo @u{QUe|l[Hs 䋈a&z qJ׉nw`)+'ah0(+z ImYY[IdgI;CQWWG<禆JtDGE?gۮg?ZnwVeSEcnyˀi=MFy*~i2d{y%>g<\/11ٌC$_;U΢?wn}m6a!?O۸9-a&ܲ `S]k{}];4o "ٽ0w5Nw%,)Vq'"<6K|;,,4ʪ*^ח3~Wy$Qyx縘n:C~ݙDG1]itkxjiqn{7lqL7?1[vm3j|}}<{#]Y.~OeiOG4!._b0g ]y+vLЗ}ٸe'l>X3g{GusFt}c}5͎ܱ_G1mɽfwMO18u:|8{u+Ou۪ktTGA vo r7vl6Y mDlL4ͶпOorrvvhb;p8mv׶m6u3WsDSc2d_ z$%\DDDDDDDD^%WTPZ|詓S[Wǟ?_IzF&KWrQTNa~ Vk5jZА'Ͷ>Z%%Y)G a,[~oxr hu4"_DɓaM:J+:;;19324a,aڋO|z}ܵ\hH0i{$wbF2&Sϥ[S&'j.|J3 =<>]ϿZJ ¯a{HЗ-sxY,_L|l׮k!9yDΞff7?}輷~“̘zuuu{mc6me@nۤQGd2 XlBCn~ͷ2/|aflL4<4{Ϟ10س0w 0 9MXq+l`73DsgPj NkWch&'%sYLJϼ.mluӸUǫ/> ;q|\_P<<ׅ^{֝ܞ?\,r[?2~jka$(0?HIf|VViNǤZ,fBCB0[Pho$aM1h'P""waDFwx(q35ҿos!A݂X`.00@ t{*ph6[9߫7 ouܒn^Y:m1y;_f"=ﳆa{ၯG?woj*khH!6nEud7 0oɛTM[>lZޫ8gnCDDDDDDDDDDDD%,, Ʉabg=.}%B|~ y `ٽ.[ε<>r5 cGҿo/sq^PWWGֹTTVvzKgtܕ̳ڽB]Z#wǝ+Ws~#y˛s_+*Yi.@H4c$&MsS۸p)L-*ɌL"#xg4a,NwK(Noc׾C,[KE:vkjYq1L2z|oCOt^qk㖝݅uζ|{K(""""""""""wQ]]Vku#f.IC^ɜx;AS&2y8v; oYw[fADDDDDDDDn] Yp80ѝ<~<|͆8|Z!<,;rr3wf'Ogj&JJ0 bcZ7@qI)&sn,~}yx%di')^)o,I^ ̏^y n݅1Lo =s)ϵ>gkg4OSC^ޔ%,,/Vʊa8GW?W=e(˕:wcyv$ǶOyE_\ϙvbx p (pͷljozYPxk7qemlvٶP[WG`@sgOgt\ʾ !>}ݾM^ cvyG3nG[G# pUZG~ cc[?RVο< Ls\5/ǿd2݆79ĩ3\uOy? Neރ3 X r: oc2HЗd {aDE/!B;qq,ee3tp*ΜJ``-^՜k{8>JaӉ?{k6l%/I<,EDDDDDDD)/"r]睅INJˎ=H?ZoG߼=0otIAa_o6 5DҮ\=b&{4v^A! ?Iic'm4.vV"Ø=sk{0s$A#sfc6s ٶ}{ӷw55؛v=3{: bcQ[[NJ71np^}Ijjku~Hp/<>f3wg㖝3#p)6m݅0>8>+֒Oxl6; x' &En*pM]KbBΝ$DmfO(*ǯaQXĻ~FD~p&7Kغs ?|Xzumbc)/ "Ø{xgb'IDDDDDDDDDnED;b2x CfcCLJä cӓ(.iF5 sȈpz|+x\eqkżg`p1#O5 NӷwfyHv ˮr5{x,n%%ӳɶsQe=!nwynwd**ٲc/6۲,IgLEK_@Rbl#""""""""@j oO{YEDGa;FMAYml1tHjϚ̊JzF&>[Nؑ ::r>75Tu1 ^z 2^`ˎF ³ Z-va;*sC>[00>>5bhyo2ZSZ4Lϸ+j7l۵kJĸf3 JTDx|TTVj޺Ͷf#<,i3瘶b6\;s3Lj_OumTq10|@v?C]ip5K 8/@*$:2v܉%)1EDDDDDDDR _D=ORVVNSGEFɌL?ګ<b稼@~B\ GOdxs\L7dg!^p&<1QAW-;]rE%^O8ZqR%u@_6nɦx`ƽQnßDGEC J:wV,7t eA,'_r-_WWυKWZ}BeUj#EGó0~}O:v)l6S[SvmWƙڽ:xۍ쫹lvφ6Μcڒ{-}=b8q up%V1ޒAnl69pؘhRm ^c7ۉnwp8ml6?f$K d2ȾHJ󹈈xֹ_EDfL{#V/Vu^˛L&2r:rrSj)juNxx{'`/IȤGO4ں:J32XȾÍuSgZVWF=oX,e7fdD8=:"ccr%%**+: ʱ6Lڴkj8p8%2^d21mNgg͆-\/`ƭdde6-:lcރWPW\~`J?bYi+>l6[=&y't/S  PRZFyڧ3q0c<pj.%ddsҏ qlܲm;QRjFQGwikVGw4K,,YkyX,exF{9}RT\ʥ~p޴{Ӱ,ZK9>Κ[aPJv1婮? #@SKu"aÿ%H3L%.x |R_~ī/>>ZLq/_RRjᝅ0zΞU_/0V8[K#ED~ٽ0E2/._q}N`ν?t pN?l@̉SL0p0}ŧYz=>[Z.4$sô=sYfk-H)Ɠ}5EKr~b.]_q0l63}DO|\ F瑇f|z32x9DEϱ5 摇f9 +˯̘zuuu{mc6me@nۤlkYn3+:=tP }z%Zd2 XlBCn~:ڗݢ#y9\3Y]et8,[KY9컑1nfϘawMBz~Vo`mٸpm"hnFG79)ϊ7p,4>>>ӯ~uy=m/xtӸUǫ/> ;q|\_P<<ׅT/pIK"_*9S[ #A<\xAJJ-4 +ϵ_Jw8&bl6d܂ڈ'F{# h+}Ӈz<|Oe[ֿv|h{!>>f~<.p8(.`욖- y L쳊*VB lsʪ*jB]cVS^Ys^Uku5AAmNn;l:5lZ r!l6 |&tv}R"" >+KGSbxlVk\;Ra*jk눎jLR fАVv(GHHpx玻[=V+*%2"v Oq9fǞݸRVWbK%x^)Ouuٱ,Y}uEGu."""""""""1a20 bii>V+2`oxnQd&6^=<-^;9ZudHε<]p6Rb|nj`ƭl\HpPA : 0=M&?OfUf3Q^+ ͩ}|v|?zmP~AH_AdDmk!ޅ>ĶsuotqxJhH!6<7MG1Gy|K\⩽Sg6lcq ⋈H@ȝp`)+8pg@3yv[จ ">6n㖩HЏS;v{EQ;,9)G 9H^"1fPƌzqv1DDDDDDDDDD\:wSZ-BBBB|vDDoΞ=z=`+]JWҕ~vJWҕtοo"""""e8 gSM6ޛZnanO>M[>lZޫ8gnCDDDDDDDDDDDD%,, Ʉa'N_XM]Hf~x}+s(9:t#n]ɒeyE"9q#""""">w""'A?tz&wwWTVqR6폧3p)nkfLDhHmͣc׾CK5;¥+X3rd כG϶]9x8>YM˥N˺5w(E<̻ToLQI)z-VxnC\l7ƍNJ>mٱ{?!,,S}3r( q1m~^M?>*bEw cF ĩLJ}6{cs:n5 O?](-Q o∈wF䋈Ap-%_qtVky$wOlp8(pͧJ˕6v$hOJmZ4=~w87o3~HfNqy\ʾs qHpP #oo:Ɓ)QS[Kxh(I ;;p5pI<,J~lwYa WrO^>>f]g7۹_HeUaTTT3חUJ?ŅKٔUeLf3cG #$8ȵ=E%%0lp*sSݸÀ7_y;tR ;3[^3Y#"<S'6vd}r%>k5lsgn&ȈpHnhAӫCnd_ᓥ+;*3ϱ|z1W?zU{C ׇؘ#kkظu'Yu8}żS:G߿b 0<=|gH}ۆk()0lp*sNXX(u>[ocO|N|ɳ 筪Ŋd_%00#y!'6;+ b`J? o8Cu>>`NS `㖝?|_&8(Ʋi.""x9h2S8KnߵYj)cMdH]]q1t"{3@Aa6`ǯΧKWp1*xGj2^wϤvYZAy?cۙ;{# rƛwL\BEe%MȐ+(ϟjm7`ρ#̛={RPX4[i9jkȾ30|@tnџgoLүO_d2(Q; LDQc)/vT~Hp/<>f3?gr"?8%lݹ|[odPXTLI"ATY$wOl2sW ?/2.O*""]֝H3S-eٰ"q1()-/WQdWrx%̞9W_ng=p1W $8<_̱;xa\.zfR`ټ}7lۍaڪ[MM%kjRV8-c6)ݓr>]Sg=y!鑔/WߟJPC;vbc4a, d/V1r`yǍ[DK|x93)KFC9zxc~[;ޞo:z.6EDlWrFzF&Q̺^?>+90 ` З' @ɔUTe^;CyxX qmev9_z$7Ӟ6F\3\/ )13g_3f!iص0iX9,זfd'Z\Z'K"uz$%m>/>I`?6ۻg|܋ak/>/VyGOnݢ8u+c&[&n<:Vӏ8yRK/<Aҿo/ NeCv˿~u$2" ]3-;peba68yqcF4>$" IDATҳEz~g7Εҿ/ۄ7kPB]A4Ց~>4j)mHW s  u]RyG[ND:itݢ#͘CI@b|,9׮ӻgaΙO<0qY~7 jӑR7};,1>?.g_eT?aePj??ɨɾCbB<PSSQj>:EQq)1ݢ\ӭua{p{!ul6;<2g;E~aUUV~·ͶO`<  4o o/j0`A<ԣ[{ o0L"00 #ƍl7铥->1\;6*$*L+hz'fDZ1&'O󋟾j6.mBpPk|p^T=;iۆRS]ө T@Aa{/(,`{&wǯ=߰LQyDhH0E l}0 -.\Q5}Ōo 7/SI]AFtFdxx1y_oysi9&ȥlٱ}-ݓZʰT״&j6!..uȹTDDDDDf)pU n{ aYrʹ}QÝvz|QefmLNhρ|| p|q8zgJt~F15ZfÁe&h iބrh:3M"Ex/""⭪*+7_jАV3\ɹFxX}YyP_o#OvVFs%)1u;Q\jΑy7X眽p}{{]7aeρ#DE_t?:gfw.UP V,D&&ss[s[rӋ]Ԉ"uiҥ.YʲL) "xs30~ O q6`ܷ`ULjhC[P󔕗ӻg|[ײrM6'N!'(4߳ W9]=NV?GaQI_Wk׭-PՌVБ'BO;ι 1,4<@|zZת +)1ئG3bg;_@EeU(Xa Ǎp4%1-vtBCC ^F\aSY嬷,4 VKog=ʬog.0GA1ev}zKޱ[wx ~nCebC7 XV7XN=OaQ nܣG=w ]n7e唕We.eo XTW{q{\CT^os|x<-ukw\#_D eYlݾP6cOWn@Ʊzf,_ͨkk~rqm=22o< k=wzkfd[ӯO/ ȼK;NdD89G5i4y<+VoEwݵD7- ʤkD>ZJtӷwLo:s'#&:EۛdÀ~}(X~xXv=9:e"EEŬXMZJ2VFjm6,]ŰCConi]EDD.e<ٿ7OCtTU ıB8v^/}6;HP`mԈT92.Ta 6(d-\w]Vzjngݬ x^ƍIU7#}QŲ,1l5=99GůXݓۦMfђ_m^}k.kgφzdeb Kf>L[[P]N_~O7%S]Mրr?Ck,)-GztCe]rIL$!>Uk7wO?F>=fflny7kmKJp)yNz0 g\~Ҳ v;pֻ [3:@X́#hݓIR7}kktZ̳֬x$S]Ϳo-󉊈4Tp[x;[vSXTB|lL!-rIYY9@ FlL%=XY$&: ˍnr ]nO宬rb q)*n V[ZkRPXDLtQ :*kҶJ"?w<쓗8lUEeUp:]\Z>JI}sCܿ9gw14q:]~b7ԥhk^/=Z%eƶ J_'\Z[[KEDDDDL󉋋4M `۶mZЫeY;@ "11j^Z|uI4>b5xoI,ԤSg+V+zE֙S\ZJRBBh`,]wkv?˲(.)0V^2bSQY]MbB|r=8]npW\RJDDx?zd59V9.aQDDD#RV^bێ=8Oe]܅~?aDTDDDDDjكm2p89rW*ȗn#k.Ԕd"8~ 2tPfA|ѽ\=G{mCS""›W;m[?"Ng]|7DDDD:>e֝=#}RA|"٘z>zttDDDDD)((hvW""n>0t!n7q4y<Ǎ]q'OqvxЯwGIDDD䲹mdn:!DEE_|GCDDDDҵkWp:ңINNRuEDv{篽M;cz0\NqqqݛBHJJ"99X@+9\]G||qqqal6 0 L3~ U[E  rHLLpצ_}|uMX [uҬ 5YoJ^C니t" 䋈t" 䋈t" 䋈t" 䋈t" 䋈t".|1NpzK"""""""""@U,%%|%"""""""""""""""""""""""""""""" :t8[袄pk)(,"=#%"""""""""""r)/"r>s{s lٶ[o/ E8u R<, 5iEw$XV(w$w`DDDDDDDD!EDEdo aBۊKN&O;s (*.o?A]/y=?Gx[t?I 4ͮ@H'@H9v4 _<}~"?KTcaafs{?߿̹ A^=x辙$%Ƈ=).)4M>fFxXNWft9O=:!2񓼷h 1MuƄX:g?Ae6.bYi=#{%݃TJ[HwOe$-%u2oRwK#[ZWO `#7y6ȖF=رgw=>*\9 䋈tS&r)ޞ?%MTnrg@>y wfr BG/`b]V$1.I=v{3}ڔqɤ$wilk6l0_84}tT< ji)]IOK!=-2=o{lL9G9͚ɨӻXa3_&( hߦz5DzkF`{hNbBk-0j[j' fx?9rİPG o ,[|˓ONQVKa<]kۭ[*zd6t I +""y4{hcŊ&Чz㋈\N 䋈t# feoCӉ&2" -Xө]9t8Q#~$CTW{C=scY5TeqYzd_zj2s5!^Gb,R8}&^=HLb?.66t|Sv@k IDATI⾻3nH^+l߽'``f?fc \mSo )1F cF]R""ewذ;lX#dt NR^QɪuILo0[nѨ0""""""""W""=3n!78.;60?,]}Ƒ@Eee9eYٽ ٛ-ALçE\?j8whvIlyna,`L2bV&-%YVϾC$%&`w޼f Ozj V܅Uk7qX $ks{fJZj2UDG1dP&]$ѪTVV2x>N!9BJrW")+ v*?w!s߲p=8vv_džMpjHLL4.\dpV h6_=ARzGh,{{tѵk_ut&FD[o> uȃɢ%+9t8/^+DFD3ӫ9}M?3wbcyYdt l|뉇` /Ϟ6b<טp),EKW0lp}{,#ʃDGq5~N9˫oN#܅)-;B#2DFDxfI߮ QTT 11hz8DdžQWWzlblV{{u{K>C\.O"r=ykzut1>7˲(-+0 p\ف@*ܞj[̳rqCNTdd--+?|~&1!˅!)1}Cdd) ^%eeDEY=,ˢIu.IW &:sZEIi6$66&@EKԩ Z%HDDDDDDD$(??8L0 l6a`F7eY._?ymjW'=@ U'jPNX#_DK0 5iK\":t{H 5k;)]4J7 ؘ&l$%| h}ZWDDZUv~(iujC""""""""Ҙ""`$E_d-r/ؘ[] /."""""""""""""""rGUEDDDDDDDsQ _DDDDD*ճgjGADDDDDDDD.DD4zt+]JW;0ϯt+]JWMDDDDD ˲Z0ͼu.ffM-: 7\jr~ڦN \ի!""""""""""""uif0 4PeY._?ymjW'=@ U'jPNX5H'@H'@H'@H'@H'@H'@H'@H'@H'bS^QIRb|G:t8[袄pk)(,"=#%"""""""""""_0ED:kIKMa!Mn߹g? mrVem'? **y)(,ĩ3]z^{{׏4n^c'N-Ǟ?atv9|Z*k[]:s|wDعg?>?YX*4fY6nE˨l-c#-7MRKKyvDn/im.jO5[##M  {<AXjwy, ۍf\n"#1M&-CTd5=l+DcۚTy[]ts8I;~4ѭU*bcb~)mkYNۭ9eQV^AtTǥŲ,*0Zo3fN &:}~?NHl$:*{n[k}6;a5wor|M'^~?M[Y4&DEEov`DDDDDDDDS _D åoپ&'.6++`=\?j8v~":*|9>r ƍqɸ\n~Я75Sywy{З[wr~`%{j&rj'x{蒔׳w?3,x=uk*'~- , ÀԔdNztrwEKWQQf1~(***0>hq+Ç _܅LӠOe強hŲ =-GfEzZ >쭜=wÇwMDzU7""""""""_|DD*''lؼ;o ?!!>IcOr~ȿl)nd@>ӓO嵷1x;/fؐ,O #wLlZ\˴'rp:];C/[œsz=uAڥ[Bn6r:`̞3`m*GYyUUU|OƾYz7ȈV;/&k@_"",9b(YPQYŢ+yws`& .[8v4Ǝ"1._Cb|ӧM|rόH *X__{^=q(,.a͆-\{Pp4 }Q7䮡0vqafoaOx;IKIfƭ[vIm8_pӦ0d<ͦ;okfzp[=@F4KJIlanVV#5b(glL)9cx{\zlIx=,Xww1MEŔgNTd$N+to4_CQ=_srFqk}^N綩>t.Im7 ˫oӳ{7{RBv^=0 ST\|j|w|> ?\⩮ >P]mv| L^nIc;h""""""""r)/"rU9hLG1u,b̘~s Wۋ֛o {vNΧ_ 8]˼K?whp7{8gMF^m]ˑojJWRkӧNrBݻaЫRټm)]y)|wgΞf8].2ӈWދEn$%%p]3lpaܳ Jf>]<L\Q5ٶcຑu}^JJyT9]{yɯ5% Ƶ@0Xv{Kwr%)[J~9Nw螁i\((d,Ν/ S\ZJYy9@M9{={ԻFY`mhh2miܛ~&| ؿޜ#G #=54x=y9>Gm" v݉H@xs|uQ^0?fd(=F+?t8.I̺7{^~wF eİfcVN\nw=q+Ys,HK ܕC5b(ᗜoZJ2?Du`ܼXEzZC7}Q۫4:6<,QzRb6jO5ƌjW˲,N9KƤ3oz&MթKRܣP#ǰoݍ{9 Q쭭CsuYN8}絚ݜܛi)]9y:?zhCr5=~ NJX&ۦ-m8y:i-=?=8s~kEb|Z(*.|E8 @eUQ^QIyy(s +os6,y[gǿo`*y}{袉e""Wu[+,AUM 1<9 B #8Obfo֛tWoCkDN-YY'p=.K](l6`s]CðSb~g>?fеK{ wKKGgޢe5l2ܜ w@{'׏-7M7͟_`PV~AOalڲu5=##"?vikM/6&:OjJWn6U6yۮPzZ̻nN[ݻ䣳Xrv=D̓_c¥|c-]Y݃.s$w;Kޱn;~ G$9"1wHJJpܤ$wp~|3 dF3fĺ}Sk:: }\5D䪓{db\6.*bcCs_IeQZVaDGEhw@*ܞj[̳r(%DDyyv_~J* kW`f1۷ ih(-+?|~&1!hn*ͻޜ).-%)!Nkt=8km۲,+*TWAtTTul|Mא磬fwhO{-fEe%nwҰM^%eeDEY~Jii[yE%q1t^a&a`0 0BIiY@@ "11j^Z|uI4>b5xoI,ԤSg+V+z 2槇ڡrI\\,qm7:*T*)1s}aybL7 㲔Ȉ"#WֈfMiNv]ڔGk猈'"v0=wk"v{o[ҖCsud{l6춴MA|*u""""m`$C-""""""""""""򅊍P{|GH'@H'@H'@H'@H'b|gff*]JWҕL+]JWҕE}//òw0 j3fĺ}Sk:: }\5D䪓{dbHa&a`0 00`6TmY@@ "11j^Z|uI4>b5xoI,ԤSg+V+z /""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""""""""""""""҉(/""L9[TTVi*^zcOBSQYB{ _?M\sH .e;.b|. 䋈\Ae_۴#ǘx9~ .Y()+S6x8|l_^g;󿐼;^xG IDATF6oIzj2cF k9t8ﲟKvt>_ضser1*:e~'Os˒Hgw!/^R눈|U(/"rEEFu.ʭ^Yx{Jz͞nԳղ,.s:]eYT9|1t:@eUmKeȠFjNWzW4ٶ0~Ҳ&t|-[YUumT\n7eeba1zdx|SZMu۸*gT:Bs|-]Ek)<XrL %k}rr2rKJ1Mkᾙd-ˢ%+9ryN3O\/7x;` {H8_dY.hJοs8f~'T{DFD0c͌="t|e-<~?v~q{$yy6',ȁAT ER)hɖt仳Y+|sTr%,Rb   Av 8}Π X|?US{Y3]#O#J$?UUVH6U<Z4V}ľa`?+/g[)Io~B~T$Rߑ}mcjoOTW˷ݤ9zzkC.5Օuy^~;yZmߩt:iuS[׹gW=M݋~-wn|Ï?wnѣy/he~_K_|+3{foD*u6S:RUUs{mzzݹX?;ϼuonRWwJ5iR>صGTizcxd($|esfV[{jޡ=M-KեbҚ3k$igO7UY^/=}5zg$I4k}OKz{{CSWky:ڦ{|L8/5Lӎv9ge%I]]C*pཽ࠮Z4<ڽWnЏ~vokj]Ԣ_A]pb]qE˫ zwB]~zT[]5衂}?IP?!AUg]'?kz4{ uƬ!?k:vOg_}]/^oQkkuoԬ9Qwߧ}WfWCT8xX?{t ҡ#zӟ7Hxj8Ǐe|)=s}k-7~ZEɤ{5wY:9e]y>s$~Z[L_VIR#q7n7JSjkݥ{FɫzmݶS˗-Ҟy4sF$nɡt|B;?w_ѕ^ݶmާY?Yg_z5oq~a"w{T]oŅͷǺ U7eH&Fxm=#fDևjBTjPۉT FY2мsևy&IK{RGQڽ7[!"?.MՊ ɓu=PSLd'A%碶f0A#x{TQ^%3Yw].ZTolx[Mtp<_Eɤ󡂋/XϜ_[GzN/Vg51aP5ZԗN+LlhJAv XA5![{%}B˖,TOO[n;L徇w+/ӔjmnG{W {< y&3~O&Ed^ݔZ}O76lҋkkԝnѲ% .obϠUQe]~MoWfPǃ5D0NGZ۔JuDܲ?;&| 杫^YfϜ}sUVZ^|MGZ5={[t }_i۾sDB洎Ԣ3i[t UYi6Umߩ _ Lrs4g ^AZ4JKs:\L>.]\?ϵwJ&+.0N}}imjsW7Oᄃl􎝻77ŹsveyoNMUܐm{W"XC+s.[.X\y$J/6m۬d2:G5-}_ھsW5==`-Y8OR=R[E6'.֥/?Wz5ZdAvdήAoq5UkN5y$e[5&3PWּ\{6U]]ZpX (169}_k׿mO&K捍*-)gHj*t__QmQwOψߢi]zw/:xz{V'oz=XÏ?GZuť[yU_m޲MGS[[eb{z/=[C4@%}mMJKJc.uwľ.\u6nߩ/qں}:ܪTSG%yښje['+#6?Tk[ܸY.[qɤsz=/ o=M=7A=q)z{ZԲ_T)VWx־񖎴iOSpmoтZW{QyCj𣽒G?˴zԼOm:r56uCOkwܚ}&*` TUVhF8xHXhPߊ ˫iygg P?U7|j=kZsf A=j=k"UVV Znf\TZR;x}߿C=l{eEE3[>lvDӗoISjL&ukk0M_,uY`jեy }_y!}ZG%Ie҅xA믻Zioꥁ Ғ?#U)L]fKe[yJ?]G7+L^=:Wͺއm0 矧>)IIXƴ |E@,(JXWcZ:>͆pe<ڏxJw9 R2܎?*TVfJ:U]U90ח .Koo~Oв k};~B=b}+H"{M{ޮ3xدp݃8Juezit:~7>hW`/p )**RݔɅ_T5tiIJKJB1U]T^f_&H+u]6o٦TSoEZyŊ3W穲 oµ=j~js] % 5Osupf͚oqG0gTWa0A> gޟXFo~Kc}-c} 1ż`000A?(  ~ck3`\رcGvzܹN;Nivi}$~&l8*/9yu|'O2Nߙd2md]Jk`ٱc-wXUUUD"!L&y^˲@[hC@aԏ' žOE-W] C 0v |;\@L,Z_ $|{/dGPAHVm?.w0KtH>H4~F<BKm y[nQ5k.rcKA~>~+p7?NpHG/$[ONy!bhV< >6`" 3q>Qߵr7 ;|h uluqF&O'~MX_>fήsu 3U`oY6/ct!?*}2A}&p:%O;G" +/X_ ?*k_|z2o/ aΗۜ$NEiG_*mǙKj kH mf[p>,ї?Y/ J?nC 9fFڲ}W,k'ؗ}aV ۖc1Cz I 37Jpz*5 +77Nn;l72}ߗ$f666+ޥi `[TEy1 VDb|7 ]|ZCC8Ӯv@?j:μS&2CG1wWދ1 i  Ͱ&"[mt ڇCۨNoVڛmu>o Cۦ>}s&uWkQq|l:\?jPwfm IzBa}ZRҘi[E~0V{Zi @?MXxog^=x,cڷMU^|*K{sy[gǙ mUr m6-q*]a;81RA-7|EgO[HC+]~-XY >[/ LTp;3K@?\Fڛa;1aQDUG O` ]C?D  IDAT5*Ӷj`&|7Vo {3wUeLێ3S: ]a~[F[p3^p>N8UܻBP&W(n~zXVoB@ܧ-̏6 ?:?n0ϴA֛9<~ZCP`^\&e}8]+T0ޛQpC"?ػ*e]aOf.C|f̰>*ėA>>*"?3'wUGUK~Ysۮðzh0*{[fB?m8}W}&ϴ}?X&l8z37+Bê]`>NX_> p z3deG 3mMDDy, o[7j8>"ۂ}[orܶsq*c_A}xLXϴYX7gV}o;j|ihDU}Nff/1}۷,B6^6Wo N0S o7eUB}A>^GU*Ӯ*!]G\FpL:3+Y@[p(L}>,7SOt6ېaQUsw_|[o7l< V6~&OjhoM:Y !mm Æ", ӎ>WxW`:mC|Ӯm~y% }p:R!jVg+}6ÂB0CoW{Xx e̻`̙]cuD^OsíȷK!A}0|7|ϘvUᛁ~VR_8Y?X7d 6t#|lk:}ˆcA:ȷUͪ{WYu,l#8 QC盡,moQ؛Ay>4?3諭 練wmق)DKxo2L|sh}3\ &?l}[o9`sQǩUQak=۱0;'ӰC|}r !-w>*}fx Z hN88UQvېfJߵm[c;PN֗ܡm:8$~a9<&nnVu8݅ [2?j}ELu>aQ 6l~p||[~ft>1=l( pDKzذq wݏt^U+^?l>}Dvs3Oh>l۵'am6e\z?m mm׶v gX}CUgew>s\ >\r \Ӷ@>,U!,j|i*_ n>n@nVÛvWݰ݇UR*|m8ngWz yWKȷ1+8&[{fy: reچ &*>n>\*]5#towmDKPl26s<^}M܀;nV m}q75kxoQC ڣT6 êߣu;,ďְR|iD|/,̷7޻ֱ-U.m{;no <ėFwh}Pi>|߽9 3h7m~ێ%||*;wGq8ۉ͏ >R0_F+зۖq=$:'82\='؏;~X~#dh}9CmQۈB?a}T8Z?/NE~uo %gO[.a|vl?\G鸕Q ;&g[B|itlw\Btܪ{\˹u7eT'hZ&*`-8Dw _*x ۣ֋@.j s ]JQAm<\j?7'w:ėF(ȗ r l3npO5>.vEWۖ-TvlKRHl4BD [<ݜ7df{> !0Qb">a?}DXEZ]$CBpSMܰ9ߐ<*w퓝#h/ +wG֟OOhtO:>P~1d/BQa@.@ϵ~UZ/ q8 7x'!|_ _v?>W?)pqo 1;~_8@a w8 1 a~va]7g=V!4A~ ] Pݱ 3E4pDWP:R}и 3r 0`x 3mG]2SHymAD4PȦ (H}*A\o*`QLTPjmF1S=A~B~8Lİ>i@nN ~Gc}$||||||Ƒ5XIENDB`circus-0.12.1/docs/source/glossary.rst000066400000000000000000000021471256046442300177220ustar00rootroot00000000000000.. _glossary: Glossary: Circus-specific terms ############################### .. glossary:: :sorted: watcher watchers A *watcher* is the program you tell Circus to run. A single Circus instance can run one or more watchers. worker workers process processes A *process* is an independent OS process instance of your program. A single watcher can run one or more processes. We also call them workers. arbiter The *arbiter* is responsible for managing all the watchers within circus, ensuring all processes run correctly. controller A *controller* contains the set of actions that can be performed on the arbiter. pub/sub Circus has a *pubsub* that receives events from the watchers and dispatches them to all subscribers. flapping The *flapping detection* subscribes to events and detects when some processes are constantly restarting. remote controller The *remote controller* allows you to communicate with the controller via ZMQ to control Circus. circus-0.12.1/docs/source/images/000077500000000000000000000000001256046442300165665ustar00rootroot00000000000000circus-0.12.1/docs/source/images/architecture.graffle000066400000000000000000000443011256046442300226020ustar00rootroot00000000000000 ActiveLayerIndex 0 ApplicationVersion com.omnigroup.OmniGrafflePro 138.33.0.157554 AutoAdjust BackgroundGraphic Bounds {{0, 0}, {576, 733}} Class SolidGraphic ID 2 Style shadow Draws NO stroke Draws NO CanvasOrigin {0, 0} ColumnAlign 1 ColumnSpacing 36 CreationDate 2012-06-11 12:29:26 +0000 Creator Tarek Ziade DisplayScale 1 0/72 in = 1 0/72 in GraphDocumentVersion 8 GraphicsList Class LineGraphic Head ID 32 ID 46 Points {289.05972, 128.49947} {299.15045, 347.50052} Style stroke HeadArrow FilledArrow LineType 1 TailArrow FilledArrow Width 3 Tail ID 44 Class LineGraphic Head ID 43 ID 45 Points {243.49927, 128.22742} {122.99279, 189.77248} Style stroke HeadArrow FilledArrow LineType 1 TailArrow FilledArrow Width 3 Tail ID 44 Bounds {{217, 83}, {142, 45}} Class ShapedGraphic ID 44 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs42 \cf0 circus-httpd} Bounds {{31, 190}, {142, 21}} Class ShapedGraphic ID 43 Shape Rectangle Style fill Color b 1 g 0.79566 r 0.371339 Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs32 \cf0 PUB / SUB} Bounds {{242.5, 348}, {114.5, 25}} Class ShapedGraphic ID 32 Shape Rectangle Style fill Color b 1 g 0.79566 r 0.371339 Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs34 \cf0 REQ / REP} Bounds {{31, 352}, {156, 21}} Class ShapedGraphic ID 31 Shape Rectangle Style fill Color b 1 g 0.79566 r 0.371339 Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs32 \cf0 PUB / SUB} Class Group Graphics Bounds {{139, 431}, {48, 41}} Class ShapedGraphic ID 39 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 P3} Bounds {{85, 431}, {48, 41}} Class ShapedGraphic ID 40 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 P2} Bounds {{31, 431}, {48, 41}} Class ShapedGraphic ID 41 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 P1} Bounds {{31, 472}, {156, 55}} Class ShapedGraphic ID 42 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 watcher 1} ID 38 Class Group Graphics Bounds {{309, 431}, {48, 41}} Class ShapedGraphic ID 34 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 P3} Bounds {{255, 431}, {48, 41}} Class ShapedGraphic ID 35 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 P2} Bounds {{201, 431}, {48, 41}} Class ShapedGraphic ID 36 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 P1} Bounds {{201, 472}, {156, 55}} Class ShapedGraphic ID 37 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 watcher 2} ID 33 Class LineGraphic Head ID 31 ID 14 Points {103.24901, 256.49927} {108.4082, 351.50073} Style stroke HeadArrow FilledArrow LineType 1 TailArrow FilledArrow Width 3 Tail ID 10 Class LineGraphic Head ID 43 ID 13 Points {95.178047, 128.49777} {100.95558, 189.50224} Style stroke HeadArrow FilledArrow LineType 1 TailArrow FilledArrow Width 3 Tail ID 12 Bounds {{31, 83}, {124, 45}} Class ShapedGraphic ID 12 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs42 \cf0 circus-top} Class LineGraphic Head ID 32 ID 11 Points {368.3877, 256.41742} {308.27121, 347.58264} Style stroke HeadArrow FilledArrow LineType 1 TailArrow FilledArrow Width 3 Tail ID 9 Bounds {{31, 211}, {142, 45}} Class ShapedGraphic ID 10 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs42 \cf0 circusd-stats} Bounds {{330, 211}, {107, 45}} Class ShapedGraphic ID 9 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 circusctl} Bounds {{31, 373}, {326, 45}} Class ShapedGraphic ID 3 Shape Rectangle Text Text {\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470 {\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc \f0\fs46 \cf0 circusd} GridInfo GuidesLocked NO GuidesVisible YES HPages 1 ImageCounter 1 KeepToScale Layers Lock NO Name Layer 1 Print YES View YES LayoutInfo Animate NO circoMinDist 18 circoSeparation 0.0 layoutEngine dot neatoSeparation 0.0 twopiSeparation 0.0 LinksVisible NO MagnetsVisible NO MasterSheets ModificationDate 2012-06-11 12:50:20 +0000 Modifier Tarek Ziade NotesVisible NO Orientation 2 OriginVisible NO PageBreaks YES PrintInfo NSBottomMargin float 41 NSHorizonalPagination int 0 NSLeftMargin float 18 NSPaperSize coded BAtzdHJlYW10eXBlZIHoA4QBQISEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAx7X05TU2l6ZT1mZn2WgWQCgRgDhg== NSPrintReverseOrientation int 0 NSRightMargin float 18 NSTopMargin float 18 PrintOnePage ReadOnly NO RowAlign 1 RowSpacing 36 SheetTitle Canvas 1 SmartAlignmentGuidesActive YES SmartDistanceGuidesActive YES UniqueID 1 UseEntirePage VPages 1 WindowInfo CurrentSheet 0 ExpandedCanvases name Canvas 1 Frame {{605, 290}, {710, 888}} ListView OutlineWidth 142 RightSidebar ShowRuler Sidebar SidebarWidth 120 VisibleRegion {{0, 0}, {575, 733}} Zoom 1 ZoomValues Canvas 1 1 1 saveQuickLookFiles YES circus-0.12.1/docs/source/images/circus.png000066400000000000000000002633201256046442300205720ustar00rootroot00000000000000PNG  IHDR[ulgAMA abKGD pHYs  ~tIME;"I"tEXtCommentCreated with GIMP on a MacwC IDATxw]EUv9mI$_ݟzmW+xQ JaQPA$' !Eg׵u6Eay)ZՇ9իW+VKa?Q/b;8KVZe( (6B1!Zk2wxq&MtqGa)S<j5}Q{ᇋ/?ɶF)w7f HSDXT h`!:Yoi$Iܹsxuҥ6uNjX,XM9ذƠiύ$we x\\ƕWk?Z51&" F" V'Hc2B"bM =|&9!Z@5>X,Yk0"܋X-n@#^[;Xw/yʷbb6/ӂPtE1&rhL<4"k(z~UzF`U5Z0=GYqˢ"n _SCG/$y5̹6m#Չ[ ,vXa]e8|HMKl>7 "O`z`-:)!DVu$?p?Z5#5(1Ic|fvZ5W~25c> |['`y~@\ĺ %q _C$ڢk=dA;WAxл)Zllhh`oV٥K03'!X*D 80&F9 M%D/#~ݹN#G`u1IN$N!1qM3D _wX]EjV+Ugeinn드;w0KN[v?;vpm0Iχld VRAyt\WD^%C+p2`! L  ^ؾWk\ѫX]C#oҘUg6##odG~0Ut4֢tAI*X!Q#6 1Ikb h"C1:U?<[Fg?]諒%ÉL(b#']˜rᔳw9)LR,遅?]9k>>sFwWwW긄ixnlauTcSs5a*?'m("?'/p!<ɡaŶ`H?qdCkIa7eXsل7*!V1bĈgk_N[s vJw#wA؊7g̞=[oمũb*I*pKn~}đ?Dzy1A̙3sb kF*1#`d3*HL`AE#g0 JXsb;?PfL^o8k-l>E :;;mRޏX~QaF#H? m'M]<͛RzWV9_ɣ7^EZk,{xΡpt $'fMVRnױa+S|>|mذa@cc#ZtыR 5qQG\. 1ߛn褂 ̟?_d2gulܸV*t [݃}D#`H7IH?낁hB6)W?^j͖B`Az&`֭l#Mѯض}ȗ]?N> X"XGGeF:UxyMr͚5V?tFG4!"%XIO (d):Wnzx /W,.i4RH/5T[HP @B>&.#,¼_P/ ٳ>@GXA,К0EtcUŊ׾uL.7 ˎ }\^V/^86@\.־?azXYqFB>԰@;:y!b*Hk T*l߾I'Yz蝬~nf Gu3xR `MBRgU统ձ wA>1T&:1oƌ AQrc>Jz>*S54oe'<&vp*lHGJjvwS*JïEx7Ƀw,>]Ҍ6!xn21+ 9ijRzVhŴ.G7pY J_}@Tʃ 0`b74>J}^>amkף~3*Ȳ`5kcW~]}ZW.%2lNz{BԋCG^HϨiHEZZ!aY`M?rKe+kòs<@9qBKs)GgRJ@0211V#ObAe<A憲zBX]#!3[,M7XHÔ#dٵ|r5DG>8 Na1=y=k1odpƍr)Ϟ<3fwȍFRBƫ!e*Lm2X]&TX@y9@3~CXfN>p{~ĉ_CB Gr-W}o<Kҏ^X`Θ GqI'jUU}QknZeƤ,Xw^t\s=p $W]kdmc= LیP>%kmB",VG踌6 a/?t4T!KYNZ Ru3V1ӟdq-2;O6OfH%xm@%U%ҋ= .7; #fwKֺ^^|tbgAJm2u\B}_dS\.},w?;q-b? Je16zjXk1P.9装y+W}+/fGhZK Wk93C.L ]ZCmZųfП>,X :::P޺ ]+R}A?pcP䠔ٜL+| b[Ϫ7|vў={8w Fgg0ax:^Mmhnn*;Xk%-[Tva+^XSELtP>,*7;rd'"UPwuK3f̸NggނzЀ(? R.dhZTlq b5;^lAS ͽp)ں|͚5V'J)jw vNt_KDQ,W )O4rqi+_V{1[BR x1L &R *@1q4SAiMC<&Y/P>i;§g߼c?T7oE` 4&e N w(+iĥ5Ph8"d,fϞ}#on{zza=w3MXB5`xQYǸ*IjxaA.[IpO;Ў8d0ul8YȽG3 P~!,C޴bnG~&7:/S~&|;jZ];^#%RcyUpCAHI̤g0rAOϦs$&IpBknwhlaAx.'1MTU$y $w!? VCʲQkABtDˑ`0PAPZ L2D!L q;T/ccΚNqrZ&F(g^jX-þ jf1c%MC U?P)RAF̸6l3E1&1hߑ>,P,r5q-[+Y HcUU:/pHUlLGȤ7zPW'43Z&=I'$"т=ؤ>VW0}؍}*%v;8T1I ̧\bWa#RlXk5gS"D}L1jPG j}߿ZڙSr%p5@9Wlcb:/l񷸒y%t w}ǜw0r$'nd|0e,"ka} }α+I|hh^vuG:l$UT؂ A)"yOMpR.;ZD"Um9(M_ݽ6;%DXѵA X1",5,'!B΃4a i=yuwwD(A#6ܡػm?;K/-<.dUJػ/~y YUhRCGxˡ&dG!?`bƌ%+VI] 5Hj^4| A0b@"ȆSJKH`9^jTx"BU M8_Z.?G!TE ].<ۈz,{΁'2bZ;QSyq]럹 #X2! ? <>Mߜ \MS9G hbWj sDEY_UǎJ9DpȄl>DWD_&{カyă!):h> \Q'w!˲;sӱ%2%>ue;&ty،54WzFe4'_itWYV[cdXxAHSfûk,]_fDrC#XkJ/K XwTz2aw䧟2_ɴ`i'w^:mwRhy MQI$P8 9|eUV١>PA#&.SYVlpjђrjCx"~w!_kOk|" TA*wW 9 }>V.Ovr>vs&a Q뭮37]D$>X B#@]BL Im:;T!<6pqks\ɴOZA+@8w쯨F:J]B ,˜2IJnZ \1Qf/{[{Lh]Úu:k]I+j6vkYڤnш 2Ę3i }v.~$(ۿ?dYZZZhll| Eka߾}  ',s/R69q23x_Bc]lm 2h61C_CZeڴic~ smooUg .EMOMeI*=sWyOl瑫"/[aPm F$5SfBiۀlRNu$\#pW3I/)B`͚56ޕ)ݸD{Rj`22hD*}] t[*B3cJe;s)y0i$&Oxb,jT_Y2RikxAΩ He?EǕYIjѯ!db_2 ۶m(}{ѕH,2hfT6L&Ü9s)yl2҂4.jue1;udd\lf e֬Y ?S`vCw*c >c4E^1g׭[g,YbPto>Tvɒ%n?czj,}jȩC>gڴi'8C!21^;UJg]DЛ}d" U=J]]]vqLKKK}1( ) ~>;L8naY^xak"d}'̡|8Yv}.{챻1(Rr:ENu&FzPua{IRaSH*5׀O6E4˴3kh"쾻R%Smu9TB-:_ww7Lӧ/1bjn-[f3 YzhjjrI [ڌ\[ )atCw#T";&Sp!My!=?!֭IGM}?Z6# dg\ŋ0v{SC@Ru2?!sc,pVO?uGz91wmmm%ɠ]3g~&Z$u]#׀Hi{PaF#XK (;b*u$SxL, G(/UiO."\ AiĘ3ރ^I:%Y$IXv5ʱpm1TⴗNe9e[R#xa [@ K`: IDATyT** ]%.*[w෶0w_fw`{a{AC ) ѡ rDt͡x9UAY'&&j*hJR 4 P^TSuh;9(%ikk; ņek{tt1dЀ ],,:=(Yҹjf)5&Xu8^@֔ ~?y*eh+ ކy0#='#bԦH&r(am2eKt}U:qY܆:o &&9JЌjAb1 ^ĥKL&o; clݏSj]] XnQBmRH>riq*>G3q I `}BD`j}șT) I'UGi6!ql[$?ش[b* Dn~e22;Q$R&Xq/O}3l:!]}}cNP9Ű$~QEqQ;L;`|M-N{/}W4+G_AM\i]X`^.8|B?7}"/?ptsf9ZյAK vQ86md_Hc+V/^؊QaVĊɧY|P Q*d*[ *#x=:[o7Z ƙ yj7ŻdoG KI^0??L+D^s/Aȭld']BST vh譤:-8;=9]@ %Ee4^>_>~ЀqԔ ]*>GЋ/^s<_s~< _ʔG'- "~߲m#D](bM Fnvؘw)mސ u0Ύ[M9kh)I[K}<6:{cki4FT[DX6#b\mf];_3 uQ(\:Τßұ#ȍf/#Gm{V&==pr^ ^iyL"yLo`]%e<#j;HC6#U`"Ǻ%}t\I!\T$0&V<|ݕnck>4%S4Մ&twAHIKXo;hB2$U 8 j8\HRPh@ZQy-egWqmON&7k_P[[ڡP?#FA l3WR)Yj]z}mZ1硫'=7Id02I"ϰsCh "=煪9k< +}ylǜ(N3,yM$%r[填'rcg©^il^v?M?g/hGDfIU2X uWAHtF bjCeVRH]2D=H"r,|fd:ދISNb6u~\MZ?^trG`r1"% ^T.T>ڇ ֦rdӽ+:R2oz6#ɰqDJ+ *EL;} Z'66-~5-Z/+~#| 跆,qk.|Y>UO`p< JH?8yEh8y#6v|.î3ߦ&f;F[1u$ȸF{%ŨKJ"U.sM__}d;-Ho46ڏ7ލƱ}my>Dt1I)H/G :&֕*6#&@2A~ӛ5?7e7׫{/`tAGJyl0sI0ڢ2l2q'QieP&"H/DR){~ aigc.42-!Lvg 3@gF'TR:B>qqq;66jFXSC쓒%UQTؔ8K`I 4eTrΉmGGw{$Vfƺ/)B>QNB6xƫ!3Ca=ŅS^y)Z %V^6M(Tqx_̘-2Ea2+-dC̹:OPkI*]NGH*=計JNJu L6ow'iSwϢ_iqNä`(NU~F 4oVHj.6D.M=t{|hhkXF;vc88r!ɯn/j]uֵ&U- Ə ɇ1,BXל1-%7'tPCCrH'0izTHa(V$iij;8蠃XoblL"dX:$")w!w髸LH"ҪAнG4vHlv pBw#S1_H UT! V*07p]ϐA]AW8쭿<6<]D }-h7/=wNmBw:eƵh̑z.eFh=;9 'Ed2D~*ru H8k 3m1IbHט ;թLҴ=c1WRyb4yi]微4 54wOxV^=Odlv:]+[.#}vFcy鮟4 N㲆ƣpK5$OX3ItUMB >CeF 5gW;ws=>`݉>V;Q?!\ ePi/ۊEQAKa&W_cFPiZGxAsJF ,GJ@o3Or&t\r CTHۜnȦ;͡_ HJev-`/+K?## s }o*1邈Ob<'vT`j 7sU+yͣm!4nx (Ām2K]7' ZĊZh_-iEE,EҍTi ]E>'WqT!F'^i;d99z/1' 9VfNT]5n4CQHa9hB-Xs)3l_F\Hc`A;{Ybɣ|d=p;&FzVm 3]!pi#bM;wȑ/"#xl>@a3k=sȔ:YZsB.DcZ@>.*k [7rwWKŖ^yN+5Ust=RӴҏD2qv:Ze˪Vx:4[ZD 6V<su)Dk-*ȻUշGLa8avF6sXVvMG 4a>gsBpqljbȆ <k{E(|HRs).o}y0*]zyfå5|۾?Sxhg4I*uPJV/&.jY1hٌ<{2+ ސ9Z=ݿ }אҙ%ATwpas.!*h:~'İe[nfl鸍箚m~h8؟ PAWA7,b$ͩp`4) ZGBs+>RDkk_ 8f߾}v``ZZZs;_}$K>47SsX&k7ԄY޺5INn2rηAwwU_4 ֭C:] M7:OSGǟ@yPahw 'Ik)%+$K| V'A|i ) #_y*V5-1\.3wܗ}}}SnݺyY=[QOڨT&2Ě*hv 9z,j3|o>N=F-x1nem:pȌrTq2gΜKctvvھ>R5u6BQlyRZxAX T;.P|uۖz!>_}d ]]]̜9cKW*;wRq=I,ӧOQ<]ňB(Ë8<qx ߉cx [1.7K!ԩS C(l^1XkI CYCBUpãH3!O)8Ѣ'4eBŀN[]rڳ7NQ #+L5*TVABg:-q˳ N=1ً35'G`A auN%\"tڰ5'b~z}e0!~Ӊvm[W~ޫxի^2Yj,O)30ѿ;8k X/Ou<)g"??SsJ;wYOZ( ~O#"$2x"!Ή] Szzh51U?@#Y2tZ]20^.,oz11junkB%He0q[;roo'czv ⎟UXU)”=fϺ!-*B&DIӳ&_S!iFyIHJFeZAe$q48̈%V5V-XEl| &)%Ԥr=Gֿ9C9$߶E{"D:vOb{w@TqJӢ4LZVs8S8imI*iy`՞Gxё@'z q,dmXk)Z%d`F3IkSP>sDM,*kDi {ȸ)׉sr&<ݨS,*hlݹ/ E jZӞ4X|hst:a'^F8{ 3"pڠ53xhAXg '5atxc,Vm0aŸ{ȤG q dtZKO}oC\Y"nd~>.8?b:"Cxѷ1Ū3'VI`T@ pBv!uϘU邨kv-k3:oiQ&@•@$_wR>s 18KS{6pGP+l|c1O5У$^LŁ5TIlEU2,+3%mJf*_Z ~/)R,..g<㛪qU u].M3]}q-)~dr! d9IHV  IDATK-IwsOڣ_DTImfTڒN spIʂvvZm|5ϓ,b>)I][y^%\#iX"nD.xK7Mbw.R~_^4+mUKt֜Cz!E%*gR8S Cjg?幈6&tBbQ:#>ꕨ) Wׅι:B auNgDG^P Pc*͐j ѫ!G$jX&ęX~g;*@qN_`X#nynbA|&"ޟG4wE \wubI~#xhm,C'qw2ZUHJ Tut/T_,[VS5iƚ"%U_GANY$REEN+&[}Vr)wo+l?[ֱR DsV^W"rۧm2~Iq_cidoQ WTY* Y%xMYѬFx,V*Bt8=B N냸a(-/\E<.)o[P"Bvd,]+ť@t1iL; ^;%NmP@7hُJL §sK^lleee+Rǿ矌͋|?'VJSt:DOz[ GE T`uox5Ak'O?Is*!JZutέ)`+XO3fo!T* UqoCko Ndjr\g4Kl}$nfC:ɾϲw}r2n|Û.lWsXݪV*d _Vġ{35EЗ'0B-cpd; VΫ m5eX5idA+^z6?&*zgys'fC<=omWY^~Tu*y6~e凟Kn?sSt*o9 ysS\w`;1-`Zb>#j [S @>].졢:_ )&DX lgK;W1Y$1ʼnO=h7tBeKkζ*f*mJt2A]Hڛ=ao.TH!ĉ Q:Ҡ6'C70ZT$V֖52%j?,  W|(@;kKF^|lpɦ76 +σ}BQ7|{\0fCY\)O(7dS?jf׾|_! ]rs1e~u2c9kʐyYzM 9xnB/dQo bsHU2*3>~^juD!oaDw|OzU}Vc@37bFfI*.6d4I| $m.#*%OcztS$. TAT2UTz[ vv^ __Ӛqffp\~h?rv'rV|QXB4Uknu+sra4A%P ܈(JơTH!XfΫK;|V l1Kk^[}/^qz3JM4|~g q֤W^0ц}&mS0t.'Ιh v6wSHVDI ]l y=w ߫tw^@W_U5\#k5baowu2xvRe ̏-'6 `LLZAV[#+Ck_*: Dː1C -[vp˝$iՀ *:VAV}}-!kR~ⅳO`}1*y NȒcw /e^[HX&d|$h7<3n٨QNͩ_Ĥ]K?#QEҺ~3|'s\FŽ/Z,eaOo0zTOwyY8 ^8,ʺ)͟}lĦ錝s]T!Z(ʦ|)#+8/)*P+?~[p~0ls+Q?µiPCmg3aLpXD QHl{TI3Zqce7',)gNa /*4Lh袇3V3{ ^Gxa b&SK0XɗQ# _+ [oAIkcL Q{QQVka׵!jղa@Fx +s3^P53uq 0CzUS!ejuQ FMM>g֥#LT<؍ρHݹM=a&;9j`'7FᣪqV=OO#>I>;|7:|=s,Z90M8vLPwr^s[$,O}&o*VҮGZ[У倭@(B=גG_{y4E*:!inMEo}DfJ6/xMpѵ'}"nGT9]>pۙwv8{u><||V` sFSܔ~sVxpq!cu?񂙈mm»߲Ͽf X (Lwa~VsŚ1G5ZȨQq7( #fJPq|-Fy(Y̷gQɤߛ4EX[-ɦPmi;دA`+d<){X5^@y7)|ԙ"lK 2ȣ IuDqUSc|1$l t=' ~;b#O\@! ڇ/ߑn7,M`|v[nմ+qUҲe&goL5oxs͋Wm˭ib||= `ĕ__ſ;1Wcȿz]4( 4-5Vm6O"&:) uOZ=B5fQaYjM窫.'հgΜY>tPwzzV|7L<77($4B*Ƭ߃2ߵ0U2V5s FзS#wBhd  }`)w,+ Hszg'|ͦúNc1X@LXq z|][O}.ت_8xq_H|/a z;]0.f$ Jbn_yP<^C0s) `8FMLO*^xMWslD iG{$4n>QܦBFMTqӟ\/_[^9&Z7C}K^4ןgnOݣ(GsM5)hLtHҔ^M9JRDVC0R옋6#:y&c [,BEHapF2.`4e Tbw"UJ5u[h<5.b$ ٷ=^1b͏dIȰRdf_=ٽ)a;صk6myc|$5ܜg$S#|@(f9vda,GrQ;*_3*'ŊskY$YsVYE9? 3>R8G3"V.uݷ5^U%m˹O?_䭯CȄJ~ U W!1 :mOHxM.$MswE. ;IOs55 AZ=@-1SՑu*a[% Ozn2^T=hB  #}hϓm$ItQ4iG5UxJ?VgkE[[dY慖eؓ3(X 4/B>qMرm۶Dž1>z7;5J!BnydX[[cyG;[$liyTB 壣l8mv6[p}u\i{;\{Ȳ \ RM/¶O3]%۲kr.sSEhet*p~|Оp;5f5D~+[H)U'O?n2[onS!(aO{My]LFv.)1T1zcS>_8yy'?#-phwz Q65(tX5~Pc)"@ql";\{ 'IZ* IDAT'!cMEQSC211,>uαG>o߾s;=N^ePJ|@)D 1j ɖnA3J4x ;XZ@ Ww}n!³nsl )P[^[nSZ3 ꫿b>oj<9xk69ޭu ) ?k 31QRH֏p~HBК1=IjrTüg fS*oy.85J:1o2D S]5l3t bQ?gq*:B0ٺu+333ffg/v |pTaU0CҬr$RȱO]hg-31[lLMuH"fɄ|Y#c:}[ |Z%T`JO畈0 8pW ?&&&E,Ϡoq [Q$Na\ ?y 5-o .cWcKzy}f}MV/ 74'pvUDh31ezhvlٲn5A}|=sO}Snrr atяxg| #g?Y711yOM!.9\H< \~bqg|o1:?#3&Cm0Η><h6[hWr[^4J)&''9tF8pʠqayp8\.[%eYJUUFժuLtcut:s>;Yʲ\,˘gbbn+bF8,[p 7ŪŊ:ȝ? kfE8EQ0ٽ{7;w<_S%ph4zմJ)ZKE횆2c 2ʲmKj>E#s6?|j---h4h6ر/zJr?f 9vT:QP6ƚ4ܙ~֚y.m7pt&PKu(zQF5896>r8!ZJdΝ='OtOns#s(8{[^^feek-Nc Y՚- p,3lP0X]"l Az [ɱ"Ž\EAڑ"0Ɩ8Bzd=BFM.rt t!ٌK G542J1ơZSU^;wgϞ)m7xK~b )R"D xQQD%- 瞉6\=g8Rl۶n̆cǎ8mZ[[k_ [XXuٸ5dy>빔*4L4h%¬{,f0ʮYGЀ>"1bTd4Vz*C+^ X4\D=ZBF /cJ[#WU z^R$.h섉As'$Ӏ ! TU?|نx;c R/]-ZB"㖧E/{W5LB 1LՈtgu:Ns !bvvv-Zu>x8YKQܹM6>XY,//Ǐ3jٶGef(T#bl~HAFg#H!|r^5V5 *`MeJJʋ{*bmL~\=Z$nn@TLH l3"JrϢם1:˚q }jXŹfXdK/}1ӧOӧOcFE(֖Z["&BDY{LӘMa܃'0V:ħY^=BVWWZcl$I1Nˬڃ/C=114{X#,KsGziRf֭{?2Dc ^'N8`@c~~ ?8`MN$ t8Q MG>#T283N dՃ oVz%W>^!UVkܛb tU?Vj/717Z *"9$ LƤTpRDPR9arؠbځNMM~bY"j`ud8gpf (17pz!f-uV iR!OYUe˅VCARj OLAN`M|5Q2F+ ZYO;h*p `MT NᲝ-߃8Y-5W^yk'\ۅpY=jJ /wph1GDYamJ:e3 H o%Pmd DQ +qrAGx<<{,zֳę3gXEz[A,Чy䓞@W +~},v\ɕRXy\I?Alpk"S8"BkD$uǎKFR%@ (a*dC"TzJ;~3%2n4vRl5$jtE|F7NAȬQSL%ʼ9gJ_D^G5fh=UqGe[_ggǎڵKBQc+ V:k6d+1:"}*X@D)ܗ*Ye $M:*.}v7C*2YAjT0GPÊ`2|z҉D{˗e q>qay$<УDO7 ՘bwtNuqΆ|T:(ƔkMU 9˃u8YQΎ.j16d /;4Z΃wըEv@<]7nزsaNsE,w{lAWH*\$,ȵ˞܅[ 3B$.|ms^ N8GHS el!e5eE91b- `uӅ8)ª>8&tY"il廡NQU5a13V%! L|Qs.,b#&BzwfTW\Ŷmxȶ`;O&wc Q6%kKLˆO7E+>ERU5 Y.6YLRʢOpkqTcasFI}!̪(!3:P8>}bT:7 8u!^ۈ9ߍ)!8uϸ1DaKӘ"Gl5) Ҏ)8/08|dpDNb!Q2I5\ nm S}"VNK# 1Q !}#zw]Ƨz2an'8)E S΍)p碵?Ʀse1߯1-tWysVe ܽyg]6L!EpҢ(R}Z>ڊVkRj[EGPBV6Y&993*Ǟ:Wk{|Q|F\?l>bwc|?7/q)a17bYw}l6JN\¤=ϱ6E Yt0[l=N,[Kf PpHM겠rTy ܘj̈́}{Q*Op'"/񂫯kzoOQcZ*?.,S?1K,$G# DxiʭHhےɤVR Se KRVuU_f|'L;CĤ]A*m#hEG°|M06HhF2Ne];)Y6/@*ts'̽ce{e\}1Mid^~vA|I{b=LJc32i$|rށ@ W J.ߣX8U FPSWxjieVLc&%IdޗOF|}Ya`GOW>Ks>rGKVS M;wjP,ڤj}\NzqrP Jل- xzi-P 8/KFجK@4dz`RTPxWbۏZkiGJ\gk&>} x:|!PעQJEP)M!^@-L&1ID *#?cI?+P7"u]xl6Q 0$Xe%%M`Zxb}y_EXS}jP^)]XZ3s-\(1$6c\`Ef&i ֐?4ÓXW ˼~/VkQ5zπtYf~ϔX(u-X/˺:tUSA7}Q?I:ƶǫ#zGUCYuTܰa%2o{]=MWn*vMk˸~IvfM ͮ&+ƣ>&u%1+F`c/&kBV9ľ|3>aVm,W"1):| )j l+?ّ[rhI*=m6;nw_{1" IDATb҇EJh$ltkx 9TPT#gY|}Ǥ(IZp5LcVR[|PIE L֍@ #Eb~~;p -;*3ꪫҒ!Yx{-rgcs)CiO6AiƑ$);62B5@: h HQA ǰjO Ẅ^C3K|C; F&ꖳ!DmxHnl7^%FɃZu?/)V +>1>61)G7xRV+B=6Dt4nEJk# *NSa+b3V3ջ/|_wuWx^6b],--}ͅ(K^506C_NTs?iFѯ:DQj/y"w%;6(?"M P~^O_ܕ"6(GkJϞokRiG6X10'{؜Qf,kvߧqz`%ԔNMb42Qkw‘x?5V6ѦI^{~pE9]>z//&?y~/ >/]ɿ3>!Usl&t T dxW=kЊ<^)trB ,=<' snC~;{ҏs*%㡨69^xˆNBWan {a =JmF٥H?Z|98|p;#G >|8p h/j( /`,kl1r)-ZѦA7nsFuIRtؤ ҨM{G!gnُ~8[KZlne5l6{.qJG"d"Ň6aKdֳ'S'{_wiŇ=C\A}vj2#jZD +n?jbmm[nU>ֲǏ{,뮻Aj5nzbCJz4=*S+yH&grܐF֊q$`Ӯv?R\I;57h3:Bj i^pIԀ"ʷK6xLx;.W\Ԇh5}~ׯxnKbeRZ?ctbw?qj{3QÉaK#'E.8[_~ /A4o X,s`<>[[lϹZ8TN [l4wR#ٟ~ۥZVnZ6 z97 W^t55o9.˧nY>u1iKmvu;+uAG JL\ !~in? !Ƀ>n#4Mt:SAR;vpXXOrԴsŮ"A׊G˜ySw5~7N-O%e3x 3B}.uhu>7 0; rU$ц+p$Xj;rcCdOnPmhJ^nq8 pDPpfQ(ۈ(' mDW 83ThEs`gQԧ>%s2u]㜣jeS)eYrAlITg߸> PJ3ϼ UUTә&x_b~k_bYƂtlGHMtj 3FcF㪾eI31V4_R]ʗ?{,\{-6gXXr` m3Vl(3i<[E~ om^ћB:~pϳs\u;:\7w78߹}vZ'B%$cƬ@5;޸[%وc<,(1ǿ h,I5<35{_E1Bbc+RDe]r=N,Q@i&Ǯމ艹4iZ**m*Jg r_k:!݁9+e~~~O}!$"Nc_Q/?݈bRo腛aF\~Y!IT !Oڢnvz=L|M؁6#]"7)ڴ\P_ښ7 X\v6aP=tF˫ %ta/ˠaYj jJ' JJ 1^Kn"?q|Φ$6=ŸnYwyԟC6%PqhO%s궚q?^ Ksiq+?ʼnoc>|tτs66 9Et=%J\།QPgqLs[ ׸ s|tN/^;~k .yrk)-<+d@/̗K|7'wIvP \dі}&XNzmTT s4ι_w 9 ! p^F `Q(Ir"36ùJzvmsxr؀G9Xgs Z.&it՘oa(WMj@dxv!A@Y\q{yѣG?Y΋QK>jE鏣tmgbtbmN10gigJLBmE/4'v^B6Q.yn^D2RIByK\pqh5x[SGH vWJ?5 0m\^rMfo18y꜍T]'Ʈ-s1~PAݠt:=[BN=^(4#R!E`*Q(|ptcԾu]F￟_ Pܘ`ohqӂt֫!U6C5\d[cBD!u"7 u**#;NDkMJIS ?%V\$3'xƜhS0\}lz{N [!LS&VIYNi)dmXdx9[T xnscՖjgsY0SFJ9'`=7^[Oj"8]uVZczQvmPE-B@DhĞs`_}@ :aUOyv+vbe& wt{ 7.H[$N1$y\'C|o tVӴ$~ΣUJ;SG#Fl.:mg@ٰ´/߀~)k&eY}WKԃ`EB-ыrR {&`Q %h&pEʾGnP e#!$nC Тm8Ds 'OCiv$(wrxQE5Uh۠ .eۜOl"~c~W1Yz`PBXy}1kO<}z4شA58S&IDL_3iWn}Qy4@~*3SNp;؛Ob[ڒC̍ $Z[9B9u7|: $iA(xsHkޤ]: é3&Qb3RJrM!q<ݢ:눊"!$Bz>M x0cbO24WTdc|IZH}4o,~M=wC뷖L@YW\؛ iW<㵍W)lsW `ehO=^$%:*tR$M'my|%4yDx ^yҸɷCt ; 7^ :6I:(Q4iڂ̔X6vnFlfZOKTuG!*[07&I]x7bef8;xWb3M 6WC\N* F\ h5޾ψqX2l 12(Ӡm ТӸ Ct'/ Dic }$'h+7m!|-7`ҘǕJ0P ML#ޘq? .HsnD9J:"of|sҡ,$pT&始x*R2*jlc4pXCW6 /D!R+M:λP.U4|O*^ݎ;˯x3z>iS0r/b+U[<{S%-hg5VJRmpPnmE RV"11$.^} ˼/\뮻NmnnbF~fe`(Y%`uf LB &hN.7I+ rY|^v-%M W;\ΤĪRh%eLQqEuN䁶1&{(U 'R 'uK)199WmXˆLn]Z\:DtS-&ר4ړߙ,ÂIyo<0?x4a[|y}6)h;i=bc K%TXSDS(ZgBv#T;ﻊu?nyFGxML6F>e6(utԴ Q֖``;n,0yjJ:t&0eKKI]nSdxOӱ]=de9fT[&V1hqYF֟hg#|5 % ]D^4sJ@*HOALߎ1~W<8ν 2H3\Q5}C]>v:T_vTJ!#fbyB+^&dLـ6R⨤*6g!ԅHߍwc~~?+A]yjmmMʌ|/ h𲞨\Ү,"{ |bmaQΚPƜt!C]x|"N_H\A6j{I끙`c#a=!PesB u $MLfIJdjNE6'y&|9-cN+`0)9*?M0^x-ϷNIr(JM=ƴ1yK۬ vrvp=؛X;9W&2 ?M66$@Ͽwŷnr:<|/T )9@ꂀNY#./ 5݆IMhC=d3"+dEq{w]#[櫝x^FlQS^$HbǠo HΈ2¦(-!>ar/VB zh M mq|q mnB=|zljg19Sy@&6I^GXB?s"_V6fVͱZwD2ygi\"roьYs{`mݟ< )nw=0zM:MM#7A阄+!QTsÿ4\cӥ*&_v2J+~M޽{y>cBnAMGej߷]9q&'Ddbz3rR%f3\֓ooB:O<.%Zt7 Y~9n*e~MN>!Y4o+ye[&q;tҦ/=)ʼn$eq> ݶ1Oj?hf, R2IA}䧧Nͼ \u]׼d_8ZM7ݤ/t E}Q_*8(QX (Pz$7 oa8.Q/7@L*bL.$GY)>cON#p,B-n.f|y_K9uK6.Y?ڧ{@~8 $h%cOYPIrƭWM1W}.>}B} 344x.u99ˆ\{Iƿ,/ L&y&-a.ݎG\w0r-p2c2{RN2ɀS-78'TN,q;qMg.{ا?xӡd?¶78sΨ}BhSk* O -):~N2B?{"C^2U VJWB=SaqqNC `g'u(slͨlp INnEjnR#JjIBdaq`r"blsMBܘBA2<^/> .܁ԕсѓ?1hUxI? 0<* i~IG8x}&P 5φd(Y]hy./X?OsagK{􂌃g咄y5wHq-;_/)xb4 8 `R);z|^ -RwT91#Ricӌ>5-#~+,rɾxcc&h !Kk[FD[%QzzSE/Jֻ{U}9{/;_;3t], PȐ8v"&V:ب﫼f}rœ ,x7{@%+Cr+C-N=gݳ?LFw'-\5VE!Ssu䟚JfXv~xcV> ;G%Kiu83 cByZK;ؿɮ^`| .Tti1ƑwJXn m.|lc ѡ1-p9S AYlڡ,`&Ѷu>wa%*gCEH(o2J.umaaPTJQ]jGXq͞y8˱0$ i.DY\tԺQIu^WyWZ9r$Z=ASu99oshbm*dD[vJ}tuL|) #D_ZliFaDםMmWRroRնA=\GkR661T &3,q|M_|% /+d4.zfO>,WnONE~TP֎Fk)!:mN)AirD]JLI# ܐVt/6%>Iz qh2MIT3B %{aWR7.ٛ^wC;:Gԟ2q;c ɢ$ !; K?ϊf"(OZHɌ$'eD>#xjQ +ڠL4|aNP]ܔ?LUnur?^'W˜>Rla3Q\"N\!f懶M1'0 ;_O;|+M_yoY{},YQK~{-jv2rDY9-6aha2%!I"(u)^I΁1+G2ܸ80  0,ޏ5nP Qcjb+έhywbqTr`*gO 7k0d,GƑR$EZ?=w8ƛ.cknGÐ˭פ6W]$MK8.b٬# Xތv,;^nvX|1*?㕈0z!Tcd;Q*ǫ&΢.^%;bWhE&X  2M(Πǡ\ ajS(m1/$]SISa#qig<weSv%r[h(&.Wx_@rB _Fވ \?>M aѻzƃ2'TgkkHQ::^΢X"P}褨GKbý᤟~۪X#Mn*ᘮdf 2ky%aOrϩU[_nW54tw2 WQ2Ћ= mbr҆)tKkrQ=Ed&*߉tc!ۇwct.E2FCdߎA֘iʵ:LsBTRyL"?ub{Qג8'I)GqC'ӎƳ٘&sQUY\U"OHtM7b p$ed$-n z?n\Ncmmcǎ%j2oxGy$2333 u6&ϲ̧8#=xf_j"+EaOm"7%)#h7>NTFkw I6v+(J̝Gk ؏ê}I>3^C(_x b:rgqx,Vz2;0KtucSTU1IL7dY/~7cgITE-̴\(nM.eYh T|h t'ّ5TcM-9wzfb (ADHۊ+lQR\q8U)C*IE\I,9XkLQ)RlQ E"Dcok^CI}{9{_k[;e.f3uS_o#w|3 4~G?|̺΍v@@+eeKGYeNCtqϏBz qѩni5UT/^F7RWs2+݇ik07rJR3{H TE؎i#L1ߖ)k% _X+QKk9I{[g !9MP5Zf00 ֒9~pڟJǗgE8~n<|>,K9^`0( B{UZys8r[' R8矿w ;^D'B=ua,2XIbt2^:zߗR"Og벅dyk*7;#<wvv H4cD?I#IwHˑɹ9W w|ڃyA#;ZԳ[;y~A)ήFo6;*EpMZx>ӰJ/0uiv֙mJ܎]`V=9*]j#V4CVWWZ3^/mz_{}si{&̀!]ˆXUQ;mvS67VȌk#Ss8?̉垧ȴLw3y"WLߐj2eQ{d݊:t(]9zU8ql9vNNbK|'eJbۚuiFh (E&s~N&z ZaV klc Cfg];{(cȏ}eNwX4lI `R!#Kz/qvB*_mumz..򭬬|·?n{>яƵ.:ٓ<ǹ@ԢɔhIw]hQd6,#5ϯpfsr2,w9}^[":ɭ"/ Qߦn,ӼpeGTudspR:)l&JV \V&t<eW6LURҩw/ݧ,Kx#Z^v̙=Qg@tX:;@i;8efм+~/[D55;O=sϓ(& ~sO =s!B3ɰn".߃FuLf u !ཧidssm՞JjA(?O2khجGS*s8ggF&e@+hYVl[6"K# M C Y >4jk7Dd"|i;@CtoRZpQPfK?#1f%7^i}nCԕLQ@ cgƯcGϟr-nT4Fk"B .W^+pfHvfMcbU9y:~:+.^עgQ>@75s,W=W";&|F:0f#A2S'z_̉Z~WwsgbSP|x:X8dGsS񝧹5q`Go Z˲9lee3g|_mւLJ)/̀j3KHF= Pz*` d@m"F"d% K(wIƧ{kS r츴T?IETGnx?B`6p 7pٯZkvvv~駟#4_x7_.Ts4tR>ՌJhx΄GŃoǟh…SZ 6,a־л`ɲ,(=eYrxxӧ9}O7>OEkm9PQ]Ej M56G‘bd943l1莴ѧIWHV7V+n6tNS E/=׿MƧ(իWyT i#Hb#ƀy/'O$LT'BbДlsl61藳B=cwœY' >u 7mNWS{QR Ylnnr̙_[[oFRpl3yw{ď?L>@E - SU"gJ:(ȱxnҩd$FH[,972ȕW`38{&QGlLU:%^}OgMhimIzT h'N:6Bd 7G!߀mdwLO"h_+([Q+@q"5S gޫ㈭=C,KFQKuP1C%Ԇ2De)6n9ۻtnΖxvQUgϞnP [4\|9>S(bmm-mX˨*9oD76dzA*,qzE##5 x<*Ο?Ϲs羮+At'p84ǝ%B{hIBJ7,~vԖC,qqInƯzq/ou]\1FZuOK*!Zauuzt^ gooN>ɓ'Ksp& 1ݹǫNoT^oq-/]/^`0ZC Lnquf3)ۜ;wN-///-kf_w{˗i<ω'%t;jt7p֚{,N&>ϟgssSeYx [WnX5cehcF>ۄ7c6ބꝦ@##={VFE-la {ez衋hm L*j6X}#,ߋUc$@=`kknA E\¾B{O\Bx#{fMӠ{O4]N9)51 Bm3k-Yag\E"(~lgg>O:6ˈ. h6B|;^{f4Ak9Be(˒d'N_7]jW_=ι?S%u]w{R`@Q]@1CxRyuֺۚmliexĸ}V`9m{оGY.C,1X^^VEQeY}AAm|P "Ji]BRf2bM݂^zFks:֚m666VWW {9q$H4TUSeYBUU4Ms累)  @䬵]P8\^zSǃJx0h-bLM+M"h8OR#ա6FfibhJ_iE96C ̚b!_0 *=hC] =D˲^+˒,XYYip,"(~yzd1\=ܳ(+_iɉ DBy]m08 QD@[U)sdB{}{{FX߸`Ю6y@eYR'kfɄ:Sey(/•=mQEJ+P%t UoP*G95XAlpA)0p3Tt⿀o(!=BtfB &Iߊ"!zP#t"DJE4Q@[@A!Z@y!`PʠL:#D-gj{ %1dU!677y/EP,yce:_'L@諮: n2ҫ)Jg41:遂bLLA4#_E;KDeqNr ;̰]u]3NgX _ESOg}'sn&N8^nH[UUSL&]0fx jBi_oqNM)'S >84HcDk*hAӠb_BlψD z%)D7%4SV( e_o$@5b́V[SЦ A$ѡuN1z ;@iLJps^>Si 8 Q*VSA*) 5 *0DNP *_FeoGU9b>60:hxUlmm-sOq>,iԥ_?W{|EbTWBpl HhQt! *hC7Pg3(jeh]xi|uǭE2my?2柯zG+++]n"1Fs=_6VRMxIOBnCl!d}+hc(`БǕpOy~Ϳun+?᜻9wwaL6Uc7ED_+(+.>L&|HZv-V*Ch>x|>`L(fnyR1U =|EpSs M]w}b3Xp_noSb&*K:Dg}D <6_Aٞ؂AR&JDq*m1}$DXB B3Mr m{$ɗR#R$ƀ= n:ØРMhLlC%@ǻ)1xlo-ݳ וIe2J4*CޗĨ1KP6oD .@Y#~~Ѯ뚵5Ξ=+++ xxxC=CM?kOMF5aeY쮒ל8*٣BCM pLkKtsYrnMx ~h[`(#fk| zvUlcqZĂ([QG|qCﶂh9の%[D]ݦbii8^qzeYv-N[1OӟL&09<wU0q(x 9'b1'L'L~mo0Z>;3SN/ۥ)Hq6qxxx< "x5k=شvZy@Q8B=53 5';P+(?؜fDR1T)ZR-뙀+wQʠM!\Yt) Ǯ%8&9:H4РRCi $4s u^] RiZM6 4 Q:Uvu &^~_Y|gv$~I#)_KUkxFtޥ@;m6,_#Uj&VelJ.r 54!g}P{eG0yBlz%:=_--]'o&[YQPAm67 7ܠ"b~q61 djp~cW_IFkL"|%ӶB~j?q])d+Q 4Ld}y=8![Ź @-v{*Dx==B Q:LJҹL&#r$ j>5*)WKJ6q]Pn?8l:qt:{Q+++qW^.]b>cyy~c'lJ} KԬ$!3A1xS|A Wx=*__A~G~*9q x4r/i~E4kLp+(>9 E)>6 4iB,UJA 8= (:_h_ bU*5 P:ׇѦHPEL:OL`Rƥ7-21JO6 z= jM3(~F뼣3b 7KAL*[*ઔ>Fu14j n9Zۮz$C~(pJ5iщ)h+h$)Aa69.WzB=#lD#>F)L]d .%?tV!_G o;P> vs9\pA----WjPl|0JG]? /_bkiAAe}| T0ZfF!F|yh㛙Tu!\ eR6 *&#\=FȆd)RDG]S89QaF`2l7*մy_$63dH9xBhjU%6_FNm:b+ly֚z*>syp8d]5.];?Q_ݡ#T5ΣE T obsS}w]={:ej<uc ]#ѣg(wr\#^ VWECAQ!Cz&i>S;׵SJ"ݿJK(6mc KDrw)JɛP+t6Lt$HkA]'ԇLW 1Ej*t՞ZM茕_16Ggcĕ{)V(a띈^@(Xr/ BdTkt7hmS<5PUP[PvO]IeRo!b_OQڤ4$Amn.{G_1Vd[Z _=Ĭ|'qt'1?%צieccoy _A~i677SfQ GnFǘ:ZYRXb9[2qLiJ 8mNIz"亶%E2P#n66POàj04M Q6*:d}j.(kP<6!7R-"B T.SdE/aBu啫L?$ՕkYIH m6%kTf!3VW(/3h<5~6?_B} 8x'P4PkP]!L.*U5. |v2Tz2: mʐ'<QP6mCM0K]TJ/K #!Av%G!6=uC%`PbBlmNpU~ZYR| PT HJ%..kkfJ&Ǖ{ Kb^"R_O2φ9 U&sZJ2:VT)QQ VK~O'%}-}7QS$J": *R~lQZY_Zdm=zkf${!:Nܹs MO}*6MRl' jTCP(d1&Gu$:쀴SHk2<(6bDɔ*e$YvukF8(4l|(C|9O21Q50))Mș5ˊQ0eեa_2OdA3#T"WiK&M+j@DCoatnPgkkkqZ)˨14?AwUJ- y8z~sp~w@`"4fU_#Ο%/f(H6DrS&_~*AѩGN%»*u+L1J$. HIj5F(t#/*/"zΆ?OUbW󄌤JWu](4](SHw3ԓT-.NHCӧD)/QbAH|}$U% |KvmeXBpRIjԬU:ƾ}(u^*U. x"4r (l+ҩ@+Q n*#?qu;feB bnex衇h4J s·zQ*dYLG⤮L"‘^I NA.\ltIl#}U:KGz༧ZB1G33k2' :!7ӛ9g 6Þ"r͢ LTjIljiSZaXMCDLGJڪxq>G>?Ǟ{{qgcXsGP k YaAFwܦ $hWޗsS}ٌ޼O752:U1TZm-@(e6VeBwQ t[a| p.z)=USȦ;M.[̤ i0@ߩk9襍@NrMJVl 6 FUڃ `ȶ]SG^ä 0z$ anB 6(c;DYbIwmo 0Dž;n\ڡ؟pxWCecoC8pF7ĝw-ٳg I,K\>y>8o:~bx-qh}s$@ae`%䒱+zGt3:u(c}"b%qjU򑴆B.,=.l$b[%eB#V:خ]û)Д `l?Uao+#nQ|q* ɂ/;qN:klaR{E:qvhxuߩCOhIx$"6@L<:6,c IXw MSL~z"׫{LfJVյUD\zc{NjQ'ARZ7!TY)dEZIJ]9QDC$WДI`;@j8u7|Z}E'|2>Ӭ { -Ol\6-FDP^d"H*8!L$K/8꯫ >Ʀ'QbEbz-M.1MWLT1ȭg8<*| IDAT za %H:mߡ!"fOs2'3$5~atEOƙ~|nd[!_٣g_'yw|wpwU[$A}a%(/g0{/< >yވ~.|7M=I,۶;rH&>2 QKyه&}ః(euܜ')1*d#=|3#ƀMU8 gDm u0g[-Ng!$H[kDf\ǝiwamRi% h2y' Q&P4m 69N\)^( /wST;H[?FF9p=ʚ030B kScHEez'$(Z1v2Ed:<(䒈I J|=:X;[U{Bd#i{= t(0|~JK )QU^%wpp.%;>3PVxԣm2}0{7zBG'ckk[oU-f@ b<^|':^x7aDWW8)ҢR#exĎX6A~8Qf*lF1%rWVqxBv =ul 7̫l^x5␵WsHB44N6-c,UH5Jk`7fs7q[SIS<(hUjwxZ;.?vΩSZaN!C7W'ӧP5b}$+L_agA憫 %GԔg^?o}_'sHm&0216GJPI@L$EdIl1MdѶ'MuHs>SV/ϿLoL*:DAtI!\![)]_ cB+\RIK1FGJk@*ԣHz1!A%$aNⴇbh.P+Szyjt-QooBAI{LV/7+E8G Xmfs%v8}ꀍi])ȑS)Icy&I=u*-c֋|{F KD$Ux !T[+4su4&JKZ ָ@ #*zM$nubi… ?^-b~㏳UV9µ?B_]i|54@ :ڱPJ#݀tFeZt6'сl@)#$q*qF3x L+ŤLJbZ)No8)j4ӆ+S˸T( 28ڡ-"6+Ϧɩ~Ց°,Xr}MKr"{Ǹcͤ_R0 ?+Hadv[Q(W*&)zUB7r&]+8̐֙l _&_I 8GESmt垴]Bu44k\Yb$y2HL$%+75h0)˒_mnnob[R;wNiOcgEn:=$Ùڡbwøq8 \ٛ1<  f󒪞S#꺢qFe1M=_TlN1 rVh# Wy}|(-+"c^{ˈutcd DmODFWWuʤY[L GeCB}z?w,/84{Qkܓ܀%BىOfT?ҋ%Pi7t^*+GrLR;(/(- IKd_F_w;fȴ[ƝK6L%s#?ɹj24`P| L~ NTBsȐԿ+X#N-ŻALٓ0E%8;oEg4"9QkbPkwNk| <)`n.lL0-pAVF%qn НpKپ4FZ*WzxU囨oCmлejV{]wɭAed~8 Ϩ牗}K&Įtv˽j׀Jj'5Kud: uByͦL+p9t;*na2LC|y(lC/i;I}'4#@(w߽o"}k}pWx7S;'M8&'{._O'c; A*9tZJL*\ԛx+K\!\.1^ >> ڳ_G\y}Og GpL{I \׉p)RkϗU:$> GM^C71Jw:'>.PYwz smGzO|\ifuI.Y$d MfLs|mec~q?"8V3:;  쾰+M%JnlEQ5/л'&@}3֘1yl>JIUP4 LL^5/Rr\Ž'6mPJQOc槸&<o(yto,|g;ۙzWQϾtv|5zr9S+4*0R" <8 iw=7o~JQ$+IdP27dMcZHvt=yn:erCȚC7sM baAnFhfd{??}o: _5x.VboX)adڰ1H5KaQF)&[k?xP>qh1 \;O| detY-l<,U D Pw3jS+u83Po x˗kڽ{Ç߮[[[g^3gh4foet4q+(i-(ADzh$)"dtѦΌq` )e7-LI -.$>#IQ3_ZŒM'"z`k \cl~XnV#?^Gm}jb jGZh Oiw`EyjH"2@NbSzVn|в3E.^\Y<u.'[> x4.UەRC|3#>J>Qb.~+?n:KB?Uū8sC?L+;مRox/o\˹{Y}[̿넪nRgc9j]= >sg{Ow8Pǒ׷⛚\A{.2tP\Plqymh&hlP M5!ڻ`4{, ^:QyF*ow(gQ+UVnpm$RJ-}%_Ϣtaww/_~.1FzZ6Np qE *륛kB7ylvQimO1"::aɔGf@olˤrER4G)GQy?Ix;pm9mOڢ:bCr#olQ=w1~U6;-q<漜U}]>*AmEʬ[젳AvJX/h!/Zj-GjXŰ?:6Ƅ2k&l<\;_eןxZ1~a:͛T7V3*8,?r"gQˋOd4a!@o|a_=J {1Ǥ1 [77uGָ'/:ɷu™9~0Yڿ0ƒ(oڋRv-ox`ߦLh,Bg/CwZͤ]3{K$윧jIfIeRQ{w1=͵/)ۖo ?e0;$i"e'^xߝɂ><^қ;Dj?s}E9LڦCb5X]3 o*UJQUm!ι6$wmm 5׮]ҥKqXgeeUVWWz2}n>uK/1JpSl2ZZ1HL$Ζ}R!_cپoH[;FQd*Aʒc̤8EU-]7e2)bYͱ#J{T+ `¬{R xSkf%.>4MyCKq‘CwlD5\k=ZP1IWDJ0%?6b09nJIr12|וT6'FZ3Rqv?Y-d)+)&+܍\Ruw~(X.D/S1h3ceDrm2_/[ICZ 6ClXS+jV}\?6yNnXfepq(%izCqKOg"rs bfŏR1;e~ y讌+4*5l >oXᵓţδ,K?p֓1*ʅ@t%WnKe;b%ќijlB Q сW52PG㘍ď\UUv_f4UQaF2 u>c2w61#`0փ~c0qY ؿxu@1t:eww\ԛd8οFɓ'Ց#Gxg+++,bv9CG1DەG F[,I &B" \AEP: /7@R4LG5#U}& 9 ^ lQjgP1%|&zB hELMXt "Z+wf*xzӦ.m8;$/%.(XUm2IDU%f%L[HA v -|#! PgdCH i[=3S.$x=9C |5OLc` Ǯ,(|Nl +ͫDъ`^&ӛj@/ qӱN%M[߶R`ZekvŰEE-FV(k\t٠Rw s.*LIQ8']%])) <5~V <"?{.PjJ6S6ct,%ɄHn2PkFCw59L*B$ IDATUͥ+d;d1K֎0_En+]-Lr[lqbJP jp6e @Qlllm677cUU7%ܘzぷ:eY)w!%pHAʃ{.+gY8w;tOA ijC#"pfﳷ28뱲B{,u:,=yğn0BPO=!BZeŀSg<>4eoZ8cDd5xSVz|e| [8T2wluyxȧɸxC ๮Cf~ g? >&ijWKe!S' nKLΈ|.J8ąrpd5 W\Ee%>J-1*+0 V_^r8I NqY0!x,7X. qٚFEl6ֈ~ւd;Y*qڵ1 ="tC"La +% ̙6='}fe$C{,o#ߔ]:3Ƙ6dܦĖ 4{h&a: ΢7>fwz׻~{:+npvYcePB" fH위6t(>_G`:FY<,>Iuq폿!`vspΏm»<.^Trd ::?v<+[ٻ(Ȥ.#Xv0vmL ;*3xNN#QP<(ǘCSfhva):gx>Re$RZtszDU5\-&WؼZET?jÔm=[,<(V+BԦ˴P3ϼ<3Po2, eCfvGUB`/TJ£ܔE >}8&ֻu(<>vNu6#GX[[|^CZˑ#Gx9rYQQt}&/$`:~4T`Äh1LybFCLe{]L+*=3|y)OGWvYU)W1,XW6~#󼇛o'AElLDk!:[~RK WS&沅Ӄ%M-؂\CfМOy[;RaczN.X*[N3(CKN6CVmI *aJ|fݔ:hኄnLp lȵ@$R0P8Ycp' Wx?|ݧo?[UymC'utVb'F5=>HA9>?a)-/U;#/"O&֍k|tCF1[|a4g4}K'CcO^Ɵ?ok3D['d?ZnRSlƏߟymylP˼FU2c *-Ml9?_K"lAX$a !6wk8SHVdzBi#E`z2t IG2BDgy{ zɻlB9F]E ѵzݍ>)A+t:Z˙3g3d8v옺rJߧcmAGZ?gP/bBM_Pm#}qFx%!PYXCPu&jfh%rf]2J٩~g {duLsfzPWal ڡZT,b8Kû29Lķd2UiChgm 2W$Ub5 6no\ LAMn KpR*mQ6OKl*|S%ђdZ%zhbSe$t 3I"IiYׄ5- wa'Qj+E.}_3@2oaT*m) ='[bĺlQ3=Q*17)ieF=~D7-6raZxi.åCHAJL$&jiyr!Y:Z3D>jżDPlFTz֕MZָ2Z5x.wlhsd9Л$d礼]an`aGE"J\}K8HkmR#៝{d^euۻۈAh4b eO/Ps5< ::K]iqd4(tՁ7tKCYX zw,K*Uqd{@&kIc SͯENZIGH90~տJw6ysSOjӈdew)T"lI1<:̫'S Ƥaل(5¼J|9(0198 .:ƛb (D)5ڈcccJyo&%6X@{f=(5,KN:"KřXULFLJiip-Rhu;az /"hbcOn_F+qFrj",qȌXI.4MTrg$>)VD^C-Jg=Le_Ol=IxkV2#:%;|c.=dRWT7x(q!.>-6s=;Cozz2;ӿ@^(Vq ^{eRK-f*Lμ/,-Ļ`K鬋U"':B[|${QMYVي;p-kPow(ŰMo-GŰ͕\?Ϣ?E/jb]v"D!oͥstON|Br>6b*yJoffZ/op50CTY8,S Ex7V:XM%(km7_R]I)Ԕad/蕊n(LCߓ"M6IB>!t m 1P]MJY7)+&v~⑟"'h\]3L{ wh_hkwuׯ>3Kؔ q!}@KE-Wdо(lEbrJ>dƀI1D:YTkҭeD?!훔$cVA5K@o21N s𲽩TUuhܼ52S~J) JM2@$8,gF:j2E] h!ncbifS|Ӽ{4c\Rl7/6H9uvn&3$mfz0p)n>D-vk<9:" iu|Gxp$dl©X‘(%l$ Yz6|LN}',[lz|{,fo1`KBQWU/^fӅtrMi+bӗerI`JKaa6 ^.>|ņDZ@Q0J'lTsi{I#zI L@TОȹ?u/4)l<eԾ[^8Yp=5Lx%od/E.PqnyŌ8p<.ɗY9:e3JKL6nvaOTKG⫽tsLvDɥmQy|x2zEڀQ/gtn^P9vfr]>W>l;RN/+,NRjj怆41F0Ul?_8*_Kz.{/GQ5x}W0oǏWι /rG[ɳ҇!T{IXT!))EiCôIdηi"KWe,Bi nڲM֗p(=VPcg4^6er|z/ $e!f2_VGl&)VFP4!!!)Pֻ9ͱŪrCbw_ڒoclD\sh¾TsNUu6[z2oT,;X۾ܞmItIԓ|V%D<]Q#M3.bP.ܲnP G+fAƖ \=&9ZF ,$ mvPn9|bVo˜zJM= K˚TNxf(#TFs 9.nb4YbFJ˅$&1Gz+CѐŶ{+ìdΚT6Xk"kK0XQpDe졩*|< Cb3o9uAc?2 9\/O7~Cej-VR<]:tDIk],m&dN$Y$m:w|ȓ=k~5NƠף5&ud)  dR[Z˨=W퉦Iu,D|oG>8S}C"N;wx>=uRJ' apVY5 H,PeT ,>ddx7#\Z*ʒ]i-mIK˥Y]D_h+6|nj*UT_Q=FLNpid-ΥZ)Ng2'6 i*HǗb"BsY lޠUj:7q \z0W_nH}PrPGy[5'[X+م$26mD:/.B߶`6˛3Ϡ_btAzV: t7d{ҥ\m^MM:Rb,ދ%h<Ֆ!juCʢ#L:J.MJiZq'Z-feL3KF\0P)/9"l|_jj /~Uү6~J6mCJa͔l'e,FuDjF2xY/rĬǟ&Z),f_0(3\xTl0Z2D-kEmޓ7b 1i0Dhc1F^TՖzc,a\55*˒N*ɓO>sk q‰Fo8ncJ?6I$|ǭY\Rh[)Eɓb</}Bp)ҩw5LDY!f"1m$OEm#TۤMaԹ]dxI ݶ Y'm;0t)jA_e1f8旯\P)&UUnJS Rb4yމW 7 PnhḀ=F@xM_7GqǢ٢U5(y.$!F)mNbĖI-OJF' v\L(6&L^J-)/w*cҜv/>(V8Ll(R[`ʈ׿zƵќ;6R 7au94g>osw9 [w4|7?e{VEur|mﰨ R7 {F>Hao858"=ŠԬJ t;),ArwHfݡzM= n&֟6P*4>ѽdT#2:|0 [//׿h8q⟍_ܤ 3oV1kEun.>ƖBɺ=Z{ֹ.nsDm0'>Tb2| :K[MLU~ng[2f%LWHoSB3S5jjO2'sjgX/>n'Adk.^[]W.?brJ!E&?0Rhr1Y9QsɌ)ԴJ]tV{m:7CRD~.-H}|d1U1}FJEKh-ؠixn~z\BoM0Yb9=KSG0^u{[ (6Zܝj"u7Y8fq@5*-$z>;VGy!$D&䨼=¶e( C96DZ`l/qc|i&ӷ\G)Qeyeް~G^&]ˎR}/-=(aas@ eZ\|t娓 IDAT`7cŘӷe {&%JirƊw>M'UX3$eJǚ?ۙѰd Ud!b˵֒!_Ȼyt:xa0e`a- 7 Uhctb&h-Q{1%tbh]tQBEr1y'N3c _1ڵk $r}bCJ_'ޢ<>1tCJ, H-KSuLUR&Bi(UqeoWۦ HL-vv ?[14SS,|%Wu"Fb?hߴyG3O |5,ؽ}MoD;,OcrN%ov#Y^I7$ϺD8XQOgBRp4d\~&'$>u7 |\޵5_vÉ-{1[,TɍE".·7UQVmG..b b$`͔n1ֆ~?jLf22cQBTéT+S65햡KQ8f;]B I%9A>Y .Z<X& kkkjYMxWX[YY駟{{{ۡƙuџE~vf̅@ӫEرAQ Qi)Ș.>ͩ77)beBoU I2INY+Y/¢mLκЈ2ETCьY)ŧYlDa:K ɥsn.1Czbl曄Vw+Bѭ [s6Yrmj_ er27岒h>MRxjϕ҄oK𮭼NMFts5Ŋ(qHU{`(M= ]!*uh%*%lyy[\ӔNvu3sKѕYX"r]&wwz;լ;  3 !@*[>x%= nꝇnR9?zi<|2 nIt-ЅWͅ%Gj^|اn`c(Fϥ>ދ3Mm8[^薯Q4H3cL o>(,K\U~7穓C>pwB0%y,xR7(#N|"~ó55vVݔq-&慚):5!Ĵ[F/;OZ {ZF=^M [ᵽ=\6,fkC˰:zLt:%EcBR:fѣI٭Bj!JX&źiڠ.VI(8vHݍZ jxeGJcf61L8s 7=yy{{;UkmL;P;8 dήהH̕_BR .%4^j㨪~ Jd}:ʳՈvnUhIk4ݮ7wvALL^-QNy b 9b}Og!w3 zû>q]smڪsΧ.w2`3 ~&Wcg./SqP\ c2qypDĆ fyX-cjkF(͝zWm]qbUH"vS NָBƠag}3|b}ӜD TprNŌ}@ i/B1BUtV:^Mf!(b#!x*q>CC,3l|@Tc)k=e()S&p IxYLt"qGm[* x^f $|ƦXQ*޵ސ<v-jC0\hVy{!80{&"(bUcS:uBoڠ'+1Kܠ2m>oCV_zwLCs Cn>3~ޛmr9.({]%ksV4d T/]:W!6' F ׿_xfy>g7ym8wG 1@ydK_{zOB/{UnWc7?9LW3}2(Ln׼+s#HQƙc9=d9ܡ>֨Mmc!yǗJ]/4+Py|}f}]C'S86\ t8SXpHU?HL]WlO,purQW7 = Etɥy,ϫjYW>#bvXv0{c%ȩSx衇FVC$ҦzbM1IՒJ+Κ(]W5M=vE990r4h;l+6mJ͖]}oNhE`EQ "2%IόFT +#jxT呇-1(Ld9pNkyi$e 򬪮}=]SW&|KWEjFy9/6;1꧉(i aFQ4[!ԝG? *⌢2D1$eb Y鯝X9jov#/(Fxo4Β!TYʉP(AKG4e{GxjRqkgΉ-anJroOkԒN).ezpӣtYŮIZtb1֢I-̯>_㋂9+=_>fgc1`Wo8/ZS]fldbc("p+Լ ·=dvh}W=t+B9_H0Œxj[˂`!{gd>&h~~ I$6^ x:Ik1`i`٭U)5Ts9M4&DXyhKb;߰ ͘N;A 鈋7=K_9=1+Y@a5vqA<;ڎac߅[*Gkޛ~sQſ1ތA/mh!*h09u \Y%l6qaܫ99A<к B^*BbIT-ԉFm=Ibe61LЇ>>?/맦(tʫiI:*Ww鳄 PO@k+sHFt+^.~5IJH~XmlmK EQ1 e`wҰ3 ;SG<'+&BU:a*m"yIFfy[|C1xZCEˊ)anܰY! B{Ǧҧ<{$ρIoMfВ2]3,Ŧi{r/F}(y4>$Dh/'\k.λ o䚨r?7kTX/o޽ϿM>x'f[({oRFʤL^«/\\ ,%VCމ?nS׸i{ӆWjX򥚋kv&e Mh\U`@kVc;=ޛsD(`5q,`ݶYl NDi&QR}͈RTG(| NPA2jqȨc erƣ筯:v&]Dfq}`hS9o[>]^~͜]X݉uSY#[dcgz*{g/POu)T|r^}޳`7ӕsc2&R#bg^8/x[aVqh)60e8쓪D7hdm4]S Hj%4{!;>أǩ5ɄhUEVZBnٳg,˒~ jM$ڕ7 Pn+wK-ݪ(Bw flKS*%uRD.hQŲao`g]3>8~1NH DBܢ527 Atjk";:r+ 0~Mmzx_S5{ 9EǛG(2€5IO<]mX2PAZ*$E\R1jpS8#Aw_|t}ezL_7/‰_|XX4ňOۯT\AQT?wW18c8lO"8·H=^?ceBpl\*ex&KаOnh(إ4(2g^dŇ7xc3iQ<|3"#gV¢bIY{B4Ô:TducLfIgH3Q8?gWeVkݑ x5^FGdoq7xWkuyЪwuttt:畫/?;W58]fK!,Ceעd7٘8Âo9ܵes/9sG)0~_\3Wu "/ s.UEuNJjP e噗0Yl؝nM53_~~;2I$خ#y ^Ea.)Tڏm,ƽH[u~k_8=&9yد5_1T I|W-_#l!y*IB'\,Wl%by:[}7~EY/)>@As׾oINI(SCuG)/_+W/Q@'hHQO35^`{px3ekl*6AZ3'Ǡg4JL+0x.aymMQ}DZG?(lѲbR:l!EJZ$,nX[m8J"휸-h&7)m q͗k\H!f [tE#44,reulڨ,m$*vD#]4 mX=mꄺnؙY^9/\(ra>MEATin2?>&&ft,D]k/=pg8Hƹc{842l2iMnJ<#IMc_uZ+6MhД2S~?hY.TUţ>:E@ܹs_c8v-+e<\_^'L_BϟC^&;G[5D;;kUeT2)qTNSԚe?-_jnU;Қ_|$}eh$uN.ZA+UcꕙB&:8҆|\x+]/$UϫǖLP[)LWy|$+&s-3LCM9!:kQ'$hwR~k/z}dǞ%B8RٽQnx<)Y.zݮsPr-pÄ9yHq|[qxCstӰ9pl %14IHFkuf!5cw|=c+hY6Jal_`b-NB"}M06'C)mN1!$|T[*XĖPԩZڢSޕBfD+C!#t:~kGnђ? l 4 łixw9vr|[6lmm}qkkK, ^p B} ;^Bݿ58|, 5,LvpXee~7藹m6O'uAUCST,Kt^5NBz6r BɤQyj p&ZI8,>\Pm m+?v zǎP_xJs|~>ݗazO"tCG%/0uSQ4#MiB RmWHD\~si ^]烏g1A9:v hV4ҶCS2>ּ2WϣX* V˘* ^Xp,wn%f(L!f Z;ߏtMT"zKRʈY(Ty'Qd6㿝فB/_Oem.At~;fT.lVj=47Gy+/|ox֡Lb9v^޹%Fy-,_KӁ,Qڰs cgƜ>sxX0)1h* KI%̺!(ǨY̡J9/c\|DPS\EH -TjM=/mi!:ɜdVL*i5;fWǡl/҄|1eVFru2HrKD mg(q-K\.]DxLe8_sΡ0bu<_F-/@uܢSW%Ϙ/fi2epsg%E yp@vboҜgʃ">tN$">|s!Q&+iI,@&^{rշ e<{ ~#B$dإH *ݦM.dC+]POǸy>K?.*Ϗ| }ivUˡXk4Ƕ,gJxLN=>d{h#k ru7Ǖшz9ƚI ,DaCZQie##Zj1 y >ۂ\me8f8RTv q/ER|=\PI"Vqq$k9D6BRt=nZ1~u`j?^5>>,'ac|=B:^KFXP1,MBQa_ 43B=G!-kyqOalp]G+)ʂRL>s74 6G!+׌2(ԀuaYZşu˗޺u ~+M`{ 4%]"F3}jbld^X&~[Sv0+`0 Om@2{|bI.,b†Aw x=N?jOWdXo5B|{(8(I U_ d44~t)29.ͷUS~Ɵ{O# {%plSsx;O[;=c dFf(x"w>Dyp1v+\E"&j# յ:1BNXѶҔ4ld^'5dmlBz-H6uk(;Kq$PkƊȬn9Z++PJ5}~4qZ=58Ruxh/cLy]sӶhۦiZw_w5瞄 1CƲ`rς0gIMn*<Ũ&5د)@6muGfx^Y=N94Y8l 325% F|V,mG^G-;kӁG=-f.8c_3O"AG>A1i45UUQV jqm _[䀮'Ǻ(H?o sy_'ۮM v}~?r{gdQzZj01ioel [А'5iq'EuDF61+_Δ+bc84R3tCaUME[It^CTcˌ^6"L3-EЉb7(U̡Mb5f&OQ`.GsHBlnnrԩw~O?E摓W\­[0Ɛ$ iv?SDŽkMDkb 'j1I!4G՗9b#WƀDR1bƷbr0|#G4/}ٿLX^߱y}a9,(u,VniJ 3~ν /7Kj9o '_ޓj盭R^!Z\=fs2{M:$O;g:RUIgccUQH[UClv7?'6YEY{hCL k0 zq6^D[aB6&v)\ łzdo駎!ٴl=ۛ}yM/J Ħn7Q 6dڻFzֵ]SPNr6 J+CF?xؤw^UU1LH#GpwV .oիW^v c ^cL}}himAwk -|Xh+'ĘLLg+-&pM LI%m"8AYMnsK%T7e{oFR~$_6lYSlv@Ag(AS*p/Jٽ:g_]uKy:g[c#RbRuם #LtAJD;L@ e:G1:?Dx5B!$Gvn`+?N--z[xBM.\wEA]XkyNedYFz)Z?7|SO={xٞr7O?ܸrEP%.$Tu6wPf[I]J F *A!'&kjPDM) uUx'S HA5%s־㵫7=VC?9|h2F}Ũ襁T7V(qf_FDtC,9u!j҅t S,8WB9>7|hvO:4S61>J#f!;=! }"̛7ow'Xy_z'~ws?`?&;hۓPF0N| pf&|UX י[rj;Y a}J|Og9"D|_{$}G|>İ BcY閦û;$1$ې*;AeGpHsW_,Kf3Ƿk-~x`0teɲ?պrJeChns:'ZƬR/Y@[K4SCWQK[S-T5UkDz|RMfoV3[4$i56l S ,}Űe~f(7#mIY=mˆxD]!Cwut[]6\q%#8I(&}|S6QQ]!~Y%XAC E.'NcEf_V7in9Z+Iwo Ѿ~LF;ʉNzbqhՐ15s^Xr2JP`21b4E7xK',mxwܔ ç2ʻGlC U 9蔠^ȏB3;› 4!+C4WbvN׊>EW,nx$sE&(s7"W՚UUu;VOvOϴZu]SO wܳJk4W?Aq 1Ɉ*Y, =Ұ,eMxj`-` ߐf9Zب8#IRBZF5 FKKZőlFwάsg?).`l<ӆi\BUYG!n݂c@Vʓ6PPU#jUDB1|!`cy Ϋ\Jl\]y-.p@@'ceqV&7]-Muts ,f(/އt'jG wuO-/7n}9Gj+UJl/BQ8hH;95.Olw Y4C݅f!>bēCS` M4Eزp{ Nvwm1YfSkOpAdf1,;B؁que)]A[jqAti=-Ǔ1_" $-luв,;S_rχ=Vw<W bh10; ⮖km^*.x늎p"r[cѴ&)!w"Nx؛ּ~m+K_w\oXTyIbK8u0'yq7*?ΏE[f{kۚOFL)0f8~z0|&Mx]z5+3N!AByn;4 1GaOlS֘.nl@2T*t~ uڲE^ \uL0Gxt-XGU[8坖E 4&ߔy^pQ(#"ɶGA0IDATϔ#X  nC7e I/U dVI"1#B~;=`´3yu{;Exp9677xhAdM]]f&/K(?EMq;=U|2 w&s\9A)讀t _i.$hJefv ;?;F[(!F3DxadQvf{{O׺fh C677F*˲Lo~^vwn޼ֺ^k9',d$ۨ&)LnAMVtՏEEUA4ۈckM<%FԶz*X=K):2H]=y]-#_/ m< *?_zA{˞m7Ǐs 5ݙuQGZ{nܸ^}UlnnJ|iv( T!w3|yP@} T3Wlc!hI7 *;D3ӏ y0h7?4fYU5ugF`cc㓽^Y[[7o},KaGiUX&1%D9MqPbE_ޔ+_iU%pݩQ3Q-"i#$$]P"S&0:;3tM#H&Wh {k>>|G^3w?k׮/g8vYjiqdKgڂ:qUy;\M(hOs{[(Ի\.Y,]H$lnn;[zi'boc>w*$a8xJ;rEn" 朠o GI!&G$1LDh8@O6MEy)Ew?{)˲Y6p8Kf]7l6(H`p-Jm\嬶c[(y[V+~զ[S^Qsu zNOO a{2%vȑ7w/=3ֻ9|Dϟy77^$—qINQևцEOwoMη>\NVUU81aTUZ+l}ǎJF̈$vmܗS麕AަjZcS-^I(yt*eۣ,yQu=U-d{K+;b.Hu ]_Z@t&ϪC},NU (@ *VDb3T Fshdfs[n{Ѥ7n94WrV%P S4Um\a|Ut: 8Fr @KO~սރTD^]J>Ԉ8 Zh b+? (G)T\eӤ2[Ѕcoe%CW׆7]ߔ#RUbD'J:q%VC*URz4}80l{q1_"+b+,]h!57LZ1!^]$Uqrop%Q1"&xc"y #ĠsI=ef떛>zx">|ޞҚ#߉ TD;TV1T+_wz曻BP.%R=U{4/ʇ~&x/4df2~O5~%0ig$9AUTU(cD%Sl&:X$VVFigٱ8?ЬLͪ,spO6<[f nv͖vyS <3egEs KEDEE`N d40e1 6|~}{Q;HP7!7x7疍JpiN|q= IDg5~>3 _vxIa EUIRv`c{5H9R =k3E|Q=W~2E"w UL?U¹W\a|[J8T?+*ښB=xF&L2a (ΐfIR:M놄w>!aSppg,o}OɌ#FQG`'8{^00Q$+I8}~: ,'pBTjxEMuYqgQ?;0VQ(DP.%|ޢĘk!D섖jQx`!L/sr$Z,#D2=d;p4CS}hYpp*1$*P,t ^.OH7 +Х=ӼkvN>j VU_glz˜,b<"jE*b )T114AvX5C̜xF8?\υNP~SjDmLd@ )K>r0O<L?I13 hH;"6bAJg EFEA'X$9]<#φ[ oZ[?z e(/`d.'D( q!#i 3Vb wԋLi@IʄT2uO(2㙘L`\uEEЈ" <$A<?x.li^`񾒉dRF(2 &+Pq%hu˼Oykٳ8qa/iGp3`J *4WjzʤUQ.@F Z j+kW xjӎUuD(&10c)DӸreT+:{G&A!abԫdeL00bzʷxV)%Z {T}nˢoXo[ZZZ^ ^uu5{z+^Q[3H:%PuE+IG5N\%@"/V>$J`dU]I(kLZH&_\9PU# qrpÌ, 7m3Kor?;6MHtd4!ea`ԩ 6ZnZdYi} o^rk0@GG3Ȇ[v#?S6UBT" CxT'bR]e$ꂠ18'^!T7Ԋs;JtA-e m2cc^ޠor88{:e.iYcl>(#qsl"mM\|2iOX1uA7&evy֯/2294|\jFg0c,Q ."SJX.MTmLq%Zl4Ib!S7sdU\>ɔJsN_z&nޕS@a˃thȅh;5[J22X?~{r`R KKdq=>-8h?_A;wSs~֮] @MXe>^܆ _ t@»oTx.LP6xׯ#v,mvͫ/eְlW(C*KCMS3tIA+^(*r8,e( oykND C C_xKZiQxY%ؾo&cXzSC#_|LO,$;#: Z)]d_3}TpR[eELlƾo_He.{ 5U hfrogZjTWk *ɔ2Tޏ\uvv^;/_ݪ*ǫ50 TDK 9:!@XcrPݸnnq?N8Acc#gϞ[ntFTWKN &+MSQϫ|%Mfaa{d>>66Fss폳끫!Nzab7̎ƨH٦-ZHrCT+y/޺<;p t7uVCdD;!K)iӃhq pc,<>ՄWBͺ Mt߼:r4+]]]8pṄuYUČڇ&Q_uM힕Odݺu?؁ǹۮs`pav)Adƅ:L؊IA?6?jܻڮڡCe7|~zݯI50_I4=VG'ٳgOfM;~}vnz{{m;ٲeS|*e2-1?۳w_`͛67k?o28{gytIIENDB`circus-0.12.1/docs/source/images/websocket.png000066400000000000000000001767071256046442300213040ustar00rootroot00000000000000PNG  IHDRˢiCCPICC ProfileXXPT7r9s9IrIEHP EAADA@Q HP>kkw0#"BDr;rcgP*>:߶=>_ 7hPX'"* 10 X7?ḴՃqdx"1\ljІO~q#c&~ {?ͧ?}˟ƠmϭG "l!oav wvAܣ70ã ¶@ [ RoBozQ1:0&a>ܲ2wL]fJclD>/[P{ME>QqmhcN^E80V@(xpL A1(2 t`Ly>uv!Bb H !M2l!  X(BP9t jn@wchz -@w#AB0C#HD"qQA4!#I<b HF$RCZ!]~(qdYlFv!ϑ5OEFIP(*uuUjG=D=G-Qh-VExt&]nC'K- È(a1. L24 ӂL`1X,+Zal& ۄ}]Qqɒ5=#[&%& W%"''גw?%_"p8{\.Wk fp*6'(J)Z)((~ix=>_ÿoA6C8Kh < v((%)M(I)(PS PPyP%QPݢzJFMN-HGM>N]A}zzFƊ&4M#cZ, -62E:$]:]-=^ބ>>::-<#CCC<#Qф11&K_LL:LLLLϘ٘}s['pt̲XEYmXY/ѳd{Î`eeOf>ʾaQcS3sK+GnnR<<<RH?G̀`ΐẑQQ1̸xʄǤdTC3Y;sQ(. 9K0+`ebujZ: ڦ惭QA;:;OF-{]z>}>IEU_ Be? B sZ%kz߂CBZBBBц= OȌT,\2ݣ;c;+{2v!N3"n'1VMBXhhbvraҕdTOrQGt:}?/%#eщ'iii?ҝһ282Nd,4:y-23*sک,TV`X\vYA)g8W:$w32gJ;;w!@j!MaR9sEE9E?=ȗTǝ=?_j^Y__WP>Y[R^]]EzvABs5Gun_]2^#XSrs9Z+WXr^}ؠޘw q-j[u͗Z[r[Akl^7^4KVmەmtm9P{bzG@|K;]j]mw%wtW0z3z%[߳m =2|`PgސPcw;FGGF۞(E|]L_;/}vF֛s[[9;,;W*ky7~W/u`v0szxA")pG tp]EfGC!RŌaȰd.{x HA W`d,clx&p*.Bn"IKjHJȜ,GᤢҒrJNzIS\VQ]][^F*D-SfofUmv,ԎH=]J~ҍrx9G]SFM  a C1y76lLJlf\[*7(8&x\5đش'O.f杲Ȧ4gjY;s.r,/9oYjVfT[YR%wAZ%WL?54 \iRly#fĭOvdu*[]swT-2?(y?7iflS{i2eJ7?Wf^ޟ6}{%*+L+9? ٗF_/Kab3hԯ}/D!WQE:\@QeK(˔<ê˙wGQQ"d/Ī%%$IvYmP JVt/UJUMWhd֜:#;תkknDcۤ4֌dm`fbMU9n:}ptwm>RGf>n_ (L v ÇmGEnDsxĖO%KswP p,Dft:]Ӕ{tۙ[g[V+/**>[u>4,ܯ"2Zb}gWv WYxEI5)^Wok1ku|3Vَ]]n^{}\ zX7P(o0}(qpHhg,5^NLiYӹw&,._\VZIY}jM_>zؖ Tvq{ˇ0$QtC 诘e9h<7AҎ* cmzv5F"S2s1Kn;͜縂xJxhHg!`!".%tGQxS^EӋߐX̖ޓ)U _TPSxUTOe_@MHSXƚfVS+ۭgn cѰݘx$Tt۬ZXLz*b$ 9#]L}lpt ԉuIIsUG3e?{DjD{IIL\SCY99˹=kM*`+U\'O%MK+R+U^h~gӵˠ`BUFƵkMu/67x k#w,2{b߷~5 'ʍ~Xze&]dz?2w(p.L8((xIXw&`~?g 4p! Ta}l\eA/x X9C2 CP4}@`sD4 yCAPPr,[* +a2%zr6.Gg ݔ]TTwibhiMO0X1=V@ssICg9cxՙj5ypdm] n~-A M뷛n\ZvAZ#ROD*N=ߙB!~KлDr疯n \z~,Zx(/ A6( `BC">#rWD:&b ɅD"P $Zlv̀\'k/()xZa:MOHAHQq)%-}ә5wW&!&$k",'.yT*FGF[/;&#oMҠrJi$8lNm9]^~A]=&MV';l=y]\ݰN^~,RK@Z`_0!;;5")r6Z7!*>"a$'9qT ̵,bܤ3yEq%翗ݨRb]:L*k4n-rkؾYe z߈ޏTxQ#O=Ō<@ (=` <`' | CP- a։b8AN>]O0z>9,y4 JG;2;Te4.t& 6LJ", l1ޜ\<|BgEDW J~R/eYH- JOT]4Z{fz0Fc&ff햒VM6z/B]pMn4^ޞ#r?ۆY?<?(XUÉ_i23Ng>gTR8RWu>RI%Vd}l[PSJQM)5y].L=S*<>H~cф1qɉBof rގ,PsX2+W>~\Cf"U;i3[[*?wv2s_w)wvi%[w/?9 ?9889=,=[S a_4} j*aq_k:9 pHYs   IDATxy`Tsg$3 @XÎ("Ru).hߺZmUk)}mZŪE+u-V]d!šdf=sə*G{={역]6333ppc 0Aa CEEx "W"J]RW8J%0 sp8T8&''cΜ9`PQ}{HqɋRBrOËK~/],BiSBngSY2GQg Nz#!''>?+//x+}5pZk-?9":,Mcǃ|'X]b^o"Ƶzvԯ-RF:ȵq.N''[{(Ի*IjEU%O%XlJGݘsN"Ʋ,~2S%[ h]V1*q^up܃'xbzg˫S}qIp\x<}4+OEW:[srJ}Y6V?O?۽{'SwvAS]/.[M3Mӈ]f #a rLOܵzvэLgdC ILL^ĉ`YVKK޽{7aҦ㒩Pb )U*nJ=ЊQGGy??O?vڥbTIR 9D-5ջ:ysU>O2z< ,9^ƕ>\]8#*^ucHEEG5u:?O?ONTf Y,JRvl˲>I&4 pȑsD@D.Ŏ'.uu#"P\ޕ/#2@rr/sΌ :ŖA,.TZ&XbqLUfhK|ȸ"I f`LR#=?*nE?O?O~jهj)\Njn͐ >6i onٲQM7h<{ؾbHLqxGXs};Jb"fxKHiwk7r@8 GVn={HSÇ~nYpq@bڼi={BI'%}!rF=!C*GPlkhb܍`Qf `JzzSG}h4\{cǎ\#G(FMmluƌMӜ0aaiii^bEϗ&EbPSrr(ݗ )6(j¢sK=(s|qKq^Nz7 8zck Z\N斷_2 8z޻=pbb%D"anHR"AX\.WWWr1pqY_u-{MQ.ONu!ӺgTgu*KmsalZ/H^>*zܹZoRU#';dݦiF%_apXcV2g0rFȡ zGc= $ `A ) ,O rcVЪ\AF3d/ӓϓߛI } EHxrB?4Mʂa4rQCkL9Pkhʕvpl۶Mo ^uU+W p@ww[CsӓnҌ[~ * 8p:Bؤr@޺k^b[ZG.NLL|mW;"`dISVH=:D֕ߵh+[uӴ{7ss3f VӉ;=>v"; 1tsD"O6.o޴)tqhк:='&&644߭mDDnرczy D㘪D8FU٤~j8Ͷҋ`Cۥu3kh\"IiDQA\ZBoI/j;?"]orXݑ''7"2Q|@Ygr(ἉGR!?ap>A[O4ASQq'TE6 IF_*R@:Γr&  7E,WҜTwZ;- 0tZ5#lTEQAOhVp_~e5HQ[䀈ȑoټe`g\xAȡ -A!=Y-ÇdNbQaW7Gy-G+ʱ9ʷ״Cs_$Ҡ+gG԰v0tptvv&=p:b`K%i999.4-nwFsnw|ەؽ{3FO^Cv:t} %?T/GIH{l7UGcfG*>S`0[V4!]%K{j0 E{9rDgeq?DGҐW}a0 ^58rw*4 `^TE>*y?.͉ Zޔ)5qyՆ[u#;; 6랹+*mn{Çe+iB/jx%=A-z1 Ϫ%  I ΋[z^IرcpiiiΝp8))i̘Xw\999z@RR[9Ju0ō*_T$.w\`ɲ,1=QE u4Uj/Q:+!LS=4\.*iKy _[BS_ Vfi=:9%@]Ǖ?'')Wh4,PeJmz};քZkB5jIYW𤤊y$Q),˗oWk}kǞGkʏ֔سsǻ5??;!7OZp/m=%+TTSGPaቧ:teYNɥIl  ə4i矯P[[w@  y0&fq;AXjcmhWġKyyT¡G\x}Ň+f'kMBOkޒr\O_rݲz =a7J،tW_]9-UKНOW/={d>@b4mYaᯮUFQ3Vb5Xg_GGwE1j6/~=l߾vxYH!a~ngA;+#gsntaETWgv{'MzCP(z'Nx],kСuuuzMMM귧 < {Jį4==]̒RŊ(.U b8M") J^v6裫rUD2U#.drH+TDΝUҙ''ߩΙr1XdnßxSFSI]{vleb dr`G?ٿo뎭[OΜ?<ÕD\ lSKm*vXiŒ F{ *4r.!ćvyVVbĈܼG1r]v >\; Æ ~10B!7Esyצ30z 97; ! b 8ЮCs's};QbWiǩ9O-9>>5/Cу 0ms?.;yNnM5Q 9/d[Nwզp..@tKeeeYcǎ;veY[lNO4jB!='%%544\.R >H1ZefBBBKK@<} '%(OsF IDAT8)Lq_39}SD.^wSV}\vcyexɊ NaI<??O?OwPV29![-s8rz H$n,VƔ/RVZrU '\p8#b_NS_W0TOE`vT)17n(7n3ztŎ\]:z@zzzKF:F -zy#?z![ojF@#;>/==-k?_14#.`ERaELrLDD[x/%2Ny~T;d(eՈNe*~_f^3zzzvDlIjΛ/4ǙW.AǓ+p՞Юlzƍs.u7;=Q3"ˉ`PFgJ=s=#Pi;0O rVVZrU4M#J>^O)W}ՋNQ`'\D''8ڵkajU#-9 ;&[; J?u6u/K!X]%F[KAQ-n>:"8p wI'677{x cueP^^'vm6l;/!!aw(#@dEh &8)>3cLaіPI:|ݛwz71 yt 2[jiu&s0u$'pD71oR'=v{q)A APygIJJd'0`+++뗔c&G.{,Tʜϗص\m:U(tLUV\% /#.En}''nwy'(Pl&SM%&7R5֖ 燡l)S4W[r]%٦imlBă^4u*3 4ړQ h2&$|wyDz`0F+**rss/^| *:g j./u;dⴜp\N>΀.#NryvVLGߊU !iz v: uz.'rq8]ggaEp=sj0m233կI***~Rd燞/NsСdu??.5Ԕ6HR?mhʕF"3UƩHYUD\(rl-'o~S\oɗr9W_Q͈f]'6Rh454 GERm W7=꬈"--mȑЁGUC )C=tU6mT[[[YY0K_HY#UOY?Eu|v? Aիnꈔɳ cYV +ĊmKGUvҶ\N699s?4[׉dt|aaU^U;LCo Q8żL]+'͚5z"qqJ^Q fdiGS4 u5*͢r;'?g=%DF<=m5ED%kD!7SӝZ نcr"2l01[2SVw{#Qqrqml5#tl$.05j.jײכzRSS#޽{o.8qz^2T= iìL)QwuSFiaɁQUši9vڪ:'?gp.D#H[kuHNP+} gN|"=vIAAM#U,)Kɉ"yOL&ⱷG:x"m##'=CzH!=zؑ#Gz"~](H/!=CzH!=>Q|Dlh7i6cvKc.g{3vZ$v℈͞.#vDѾCzH!=sa@! })q P<ZB`a*f@{/ϋ !=CzH!=Q"_ }B@JH^Ō*;Tl2H<>vܽ4G jANG! up`8C`|}X~frŴ*(Ns.G] Frذa`Q[   E=N{{> 8CV.6rj!vT75f@^nOJJAAAcj6S1+ 2P|L8fB7jCjHAAD8;zn^-QP0[V-0%:]$U}G=:33+';Ky%W\~EzElٲeڵkiiILL fdd p#;\ ʖ}ꍍ5k~&th3O;<8AAG1""r\c#eK-\ +++%%%;;{Wf@ADEg\SwB{XLv\Nv=Vp\=r)u;طgyrrrRRR.Gy'|G.Ҕ464?EEEw"8P(p80CN<CwF{:)IAgϟ~xS?qhOm8ÑiDڹO`=37~:cP;G ;{p  >!*B ^3˖.\0##|zOPxB:Ŏ02a0vkhhA;iii>;[9r˰a|>_JJ3 z:۴cǎ믿> eYׯ߿?\hmmjhh())9PC?ukgyۖcݝNG}G>zYs#k'O[Fo|8pVA<4w]g֚AA}S=Z}ٷoߢE222^ٳD˴THs;︝a`a\.E%$TUU9r]p Ñep7Mqq1苪M6l+ hiip8>uT0s޿`0XTTw]iP.OO?au{  8Q^$&&&%%1`"c X3yWfrnk>r~:`t:].a8G^Yy4;;zWcuuu>O ۈb򖖖B,+99n-H=(xpMuՖؿe]`?!!adgg 2dÆ =;^0smE,KZ??iMob{̱A#˶}sԖ iK{3rc۬ÊL23ͿeE?_%7N\iֈ.~cl+xnL4i۶m---j5jĈpw|]_~OjWU20V|m]GE7_2n`h3ժLX|[3+n^c~|j&@1&Ng-`Me՞,p8AAGq"SEgff=C8pX8UUӔŠaUu:p:u p8\]]m}f0 9 w8.[ǀbڵÆSuΝw; Ŝ,uZyyy"ƍ[fMMM:ujjj3f￟ߣެ"xzk OpyrmA9Ѿ)w6zuPz= ՎM *G++ ([N[u$H5/=re?D/JPs 8<`[iW@zgTgv?}&  AADƐmrA3b$z_}4,GrH4ڱ ,{OװF$G, 8``sGݪ= brrrAAܼy+4'˗/[zzuw^ .Xdp::}[[w%d~ώO?҇6/s x1^wݷVLjj/_^jF 5y○Nz+f߹p_zio~i@ ~XZJ6j,@R+?l5&Uo-7j Uo rS^\V Ts=ÌǍ_6AADƉr};TA4=zm6jYo},-uLCt|GN9{ųO "Hee\SSS^/rޥɎ14o'x" k]4-#3f͚5ۊ23MnMh^OW5*99W*"o\XXس)sQșӟ?#f_|LC/6}ˏ}e?13?}cy6]Q3}KtAͷMh.Hh>ny^^gaw&e>uZȩCSB|{]%WKfTbsT_-5{䞟)q  Jv쐫X-~Ӵ@bΝ;ϟ6m̙3Kf˖lfOx@`Pjq\z8Ǟح {) \.WYY#Hsss(2Mt$}:4g_/MRwセfΜ)ʽf͚Ԣwygٲeݿf{}s8RۜIII^x{s}$$9loND{ssiR5f{ss[A61XƦ@jS忩&ԌTOU&˟q  /¶0Ewb]!>#[ v#ϙ3r]k"DScL&}Gd(cFrJ8= 3П^{]v19|nɟji9}_Rm hhhxgڮ#Fl߾1 .[͞=DAAa۷o!11155***.q`&tOjjjssC:TWWDN$}]r%]%[lYv}X pa,++;!!K]i74=;F`t=e؏C6W}'=CzH!=LD{ųPաZ;rdصoCG@$90+4.L^){'ljj* +S3D+ Xv<۪NG;8p(  蓰vͭ,0oۿz[)d֬Yo|*NꫯOo"  3}t&Of͚={,))8pqNʠA  -ZzСCR   v@mqСӦ{ϟ9^lE]ۢ  ⌣9NE\.9̘1㴨M{911f 7PRRۢ  8mg}cn{O>i֛ Ǐ,s .zEAAęq8 u֍?KmLR\\lY0͛}EAAę %`ɥh1{o+2eʧ~*N9AAAqRlܸ8S* KMM ׿pޕDAAg'ږs""׿\U;vɒ%NZ`Ao"  D8 ?}%c~O3&`!o}UW.  M>[[if^^^ii)RvFp»p؁@`#Fm]AAk|9No@4Mؿ 7p Tq̚5_ 4}EAA?g ==>4={ 4hܸqHߙCaaZrX4by暌ޖFAAg$ꪫV\)>|>hԨQ'Sיw=|ќ˗#i'  ٖ~#acǎ]~Uv}sݑHt7>mQAAq%:='OQFE"e͛Lۢ  t%駟~UV]p'CU`۷o["baaMz[AAA>P[nŋ[p8~uuRv3iҤR1bƍ{[AAA&>'kt:-jkk;iӦQF\.F4ى  8wm x7,rݦi\q#Fm֭['MԻ  pǏ#"2~_]GWظqرcX;k׮ɓ'(  8|nwߵ, 9p&N{n4eYcƌAAIkKa<(^0~s՜`\ÇSs84h#DJ`tȲ-  sٖ())cp뭷Ν;dE'+>#Ǝ8?x!滍&h߲}xJUAqvsK( x 0;^y'x$dɒ[ovxĉ9O܇;Gb{ Bm1_ۭS  sٖo;c=|proq /tMbXD,--ɤغ>KJ󨾹cO:'EAA,'sb_16zspw盦 .\[Nt߷ ;M/d~/p;/W a5L% l甴%`w}71f\|Sq3뭷jk'{['NٱF~6 7[w?s$[ݷ%'8Gl'ֶBv1>/&ªxk[\ ~ؠNrn k%6T+[[\dـt  zSՖW^S}8>?~urw/\E!N ޏܘ~1J[}⟙?~XC]>-ǟ~)]O3:21۸c8vͷ~ҡuPcß- ]r:~Ѐ AAswysιaO=+rnwfϚ5K8;v2eJo""-Al+4[]7$*6u2`q0?͎e&^D y[;Vz wB֝uېc}O AAɩdas̙ハ/}?5Ο괲PַoMt|{  Np\B{~fvv]i32EeyEL )o"x-,#3K^R PCLͰL_B/ڪwݝqd\`Ǐfyfv9<90XF3p@{֮]i4aǏѣi8nAMU5/^]+mslMj~oeɶVNXB!8c __d2e@VٻQf͚QF,k2F7XXX~50EEEhx۵d3g,9]&[&n[_ߒ-tlôe߉a5XTZ6oi*}~xat|jJnձB!ؐ#rك |Vm. *ek]ޖӛtic!b v$l۶-L&Q9izÇ+ ApKDDĖ-[+uQd2$1)׮]KIIP*>o~ۜcW-X@/x޻5}iZ00`(e0\iqW0o)j7-D!wyzzB()) ud#11qǍ=:%%rEØA.d2Ad2L&ĪpGGG;v"rC.Pɭb1*(.+PPUjîNϴ} o-/MK!rh,ѩS+V,k0XMKK5k#;:֮]2 ;vk1cƾ}02&I&t:0/FX8.--ɽ1wW '_w=fCU ŌBƏBƏ;L:C}$L@(p,y; &" OX|݊B!ľK@xxԩS3lݺ}pk׮_ p7 68_b鱱98NP@p^4i&c<_RRrY1FU"~x<4GUW]IVCBj1K1K1y\C\Օo^$$$ ɉBrL[_vXdW1" 1z+Bq4GtҾ}\.8.222;;pW7n0 (SNmU[l9Kry,1râ((EQğ_|^3Jo ?jn]m4Mx&"~ 3 eby6oS|;ߜ-TH6AqWCCKaΏh~R+IB!bFF^^ ,˾?S:\+W4LׯNFcddd1bd׮]шY0@Fa)=###+i+5xW?DŽOP^13(&^^W 'V_mzyԘ ηq9lxjhO=Z^B!Vq¸Z~hf;7|'6n0as{tcǎԄ_c  qΝ;y򤝻X\vgyAW6+Wi_{ĥ L}S1=b⻥יy|"S)*"KB!XSN;vL&N>8ݬY֮]Yeƍs{DEEEѣkBa \.W% }=dV1Zvm smec_R *Uaf U#B\b ݻƍyi*犈ذa4:0̮]f|O?uUABy{۱_ ]I_VEW $+/+XQWaZ3tlWkm*[YXmLlX xC7JǖfWmgdžlnEBK`Z{J'88SxzzČ5v"B!U93_͛=<,b]a۾995W3 B!Z?d( ~\ rE&>p†UNNN||;XeYydkiEZ, i(B!b[.K@߾}srrQwݽ{wgwzq) mIJJ EIc۲eo]VV}?rш0o)P?ބB!KSTXnB;Gwvڙ'%GFF8_p߸qr9˲& BF#H ,['>tPB!Ty樨([&NqFiTBo$&&:WV5kV||40T% dàaA:w|aB!T!={`^ĉWT.`.\8r˲QAAsC=nݺ8z ɰ@\.L&Cm1&=zΝ;q"B!D(88833@XX)۶mۨQ8(ӝ/ˊBCCt:Fc20 uKw?0q9??GGG;wx1g7U+ԭѣGۢB!\}\~ÇKUU*޾}{g |hٲo"ٳ;v)j Mns0:wk!B8WW_}5>>&d2͛7)ХKǏw]Zڵk{޼ys;  ż™3gڻ-B!&F.8YǍ_3 SVV^۱&EY{+V8xc#Bi'ɿ5kL&c&44Vp%K̙3Ga~edd8K͛7楲neY\[e٧~nB!j諯3f Na˖- ,pv\|ɲ,q(feeuС) G/dvA A(eB!ĮR,|󍯯(N,X`˖-=z}5k󼛛`믿5:Bp%@&r\j֬YhB!ijHs$:txh㏮]:S.ѣRQϟ RY\=YW!B6.vX,000??ٝr!cƌ7֟|IPP<L&%pR;E%!BA]tIJJ¯Ayɓ%W7|ǘyYYYZ:vpBk 2 IDATl6&a$+B!b 2By>'']G}qFwww{yG۶msLZjY: *hhB!Nj,'O7n`N?~;Zx_`c;vZqj\;/B!b 8+Wt:\ltƍkܹs~~믿n4e2L&37o۷4L ( ,.a&wnB!hر޽K.e3fIMM"n%''CvMx'98`0`q q&%!BA [S^^ԩrr~`;آE AE N5a !BK@Nޑeٲ;S[nNzW"ƍC]hZ\r%cjIX qdB!MJc@)))cǎ 8S.꧟~6mZn_~m-X`ф9h4oB!%Pxxxdd$>nPgwE+'OVT ֮]Zj?ۼٳgw1T }`I "BK5k &}?DGG;?.nܹ wyy bYI.cI1c89B!&Qܽ{wgefڵ7ntv\ڈ#8QٳgOVlu;v|8.ZuXo߾aaahB!jldggwAtǏ߼y;G <㽾htӦMή?~-[⚰psscܽ{"BiOu{!]n>'_|Qј/q80?ܧOz6T*93L 6 >1mB!4Yp\8p[n& EEE8S套^h4r |Am) 0 `0=چ%!B=5q =W&t'O vveРAG0 q8a.}?~+ws ш``fǎ*!Bl <$??З8_6痝qjcq-"TL&Eq޽O~ԫǏߺuK._~{%4HRB.A6mt&B!5hBs$"t:rxgwfrш ReqOa0̐Wbk׮7nر=vpIqym+I@E!MҞEZR෢(T?܆J|0z5qr\әUpN^^^ϟ7(Ç7NEEE?8^7gE Ϟ= F5drAK+ ιz[vqa5G!KǟyN  zS^^ހnܸa*F X* \*r!!!C7|s`6 o4x`\Hj˖-QQQ!:իWk׮]~իR`>I:va;-KH;w8>gw`HcǺv*my뗷<[zjMZtCQvlm"DP,pԩ^x<[DK.gΜ1o~ݥK͛>|X=:q J1K{DDĸqnrrrكAV-mƌVAR 4,##d2IcRUoaZh:Թk׮8nR8h4-&)*#î#X^Ѯ9QB!ĉ(o߾nݒjT|&Bo$&&T*+)\ |||&N8aԄ 5kOl iEnTkή+)9N5B,ST;7\ B /gŊSmHHH uvj!,, KzK@e#FT $W^=z:uɓ'322XWe-D<ڵKMM8p`ҤI7n?s h<K 6O>$qe38 !ǡXQF[*V7bY6--meʔ)r Pq1Q9ez?%YRXA00Z3fLN[?omڴIEB!-|p[TT4mڴ}AE -44|b$0}׭[={XB.d2Nw…‚Zfi8:TTbРA۶mq.+c򂤨[ú*,i%Gςz:}<ڵjp܅O!yfRHBpEM4`04m'?>uTH$P(8bwݻwήۑ}||vhѢk׮T J`K!``CŲY ~!Ŋ"RDI RQX L-ݚ&44<<oK&㾡KRʡkջ&8tCQ8}<[(I =uݞᖧi7v{<XBHBD(,,FEE͚5#""͛L&jR-Z`MlݺuҐ.~8 :NJl PYVmdx4RF# iڞd*++Op\%qKG}xw|MO.ݗ#> o\YS_ )ßݾbJZy(?{_<~ϯ?'+O\(d4|+7//fߚy|붃 (B@իs2,&&,66)9t(qƛuY K.]l3z=}|$[=*HyZhTTX2 as0Z jJ(ٳ'##cu.75׮)9e*Ĥ<19&t|rf]>?ٲ ؞tWN=˷(yĉe[\\YS7QݞIqp5y  "~nx@ZBHB0s̵kHx?nݺ':ǏW*/B[jg^ CzߢE ~뭷_ʕ(op.t7*iq#m^V0znlL|v-{{BDs/W!J{be' `ߑŇ3y8Px{`F}H j0EA.??pKNp{'M!8fP(`26mڄO?yyymڴI.t#GHݸQJnHH>d)z8 *qlf^Ngzi֭[׭]>eNܡ[3\rEa2'*?rnG9NBUW;ǎ0`kװ6d m+` ̳oׯ_o>Xɓ#G,((~J5ߓP'_I8DQҥKWujnfV<o̽Y|`V]xj[K7^/{Gk%wͪ?kLԆj?FwMB!44ǩv:w/xxxHwz~̘16ma+HPXnyMRMoֆ}Rrrr{15fa ӜNh㓕鉅pmyl ~F^^^N0lذ'OY&00PjrՓjx_~g=B!MuѡC~{xKO7mTp7(**U@&I iEtR77'߿tVভ3Ĥ]#vC!bOKԑOvv/୿dJJJZ퓪L&3Ԅ_`KbJ1NNJJ0`R4iRJJJOJ&LHKKsXs3b!AV8k^W2qg4!8KKNNNݥu7=vXyy(Ra0pN(J#Ry8 *#BQXXiӦc*VZa9;Ydߟj& Bb6lT:*bAA+R^2 EFiƑ d2TZ%a&`y֭͛S*?qɞ={,9sL&{O2A,Dd4 &1LFS31#Bb HNN5j(x˗kSΝ_(\c\!}(δtqL&cYc&ʓNwP]|WڵȨYر㭷޺ "E/Er!gwd24",F|3ں`tJQںÚ#BKLttʕ+YLf0>Yf8{>|0C0q*$,[{&///++_~)ΌX_1`#F9Қ>|~ڻwo^^'\z-[pႳ{q7/^4 ( P,aciiiǏAJ*jŊV֥ݻwnn.Cw.]Tɉ'^{b<&or ޙLO ŧ ܹu5pʔ){ůqTKLL{ q@ $pzx@\oJ[Tɪ8QMj*n-ҲT?`>H8\`oRB%%;;_[m(++0aq֐ŋ'&&8nM+W3gTpyYTe owYh #1&B,?; IDATb ;Ν r\B]pgbbŋwƛ~Ge}~F 0 VyZR WrRŊ{" 4h_ٻ-B!Fb 9s?q+`O?ҥKvv'yekHÇo޼*d 4|Hԥ2&`H믿5:dȐoVR :./8Ȗgh &e_0 #1iٲeDD^_z?a^$(R!BHP,>>>٣Fb*1DRI7Od$0[:[a0~*Vڹ׏=XBB'M裏޳^^^(∍TvnpaN !B%#11FKI#;;~~ Ul6/H#J }/'O9r}(MsL5xUH!hP,4AAA.]0`{xxvjSMӷmm> p'dP 8rb0PT"88~۽{w^j۫VZᡰ2d ˲ B!`hN:9ŋ嗸FXl| %B"0ٳ端:r>iժՍ7̫_;ltc%Bi(hRSSϜ9V q1g޽Ç f,]O>1ϔ8ދbsM,;k֬ٳg۵EB!Fb RUVVٳӧOo{߿/hZ*//{"_T5M!ҠQ,Af߾}J:@ɓ'u&1 Y ӯM&L&kӦMaa[$Bi( %55u̘1 NP1 *sʰB!֣XI&mڴ<*ڸH ڊyܲ~0& åKn޼ `BHSòl[j WBkӦͼy-(QXłeY} _c pٖ-[>CA!4M ={p覄?p$+ `$ ̴mҥK-[TTHB!vŲJjٲKݗK1cFdd`F]`_ٯy󦧧{A!47otv/%ZfĉA8 "w†`5T~`ۄcB!TgҾ꫉'K ##CR.d5aa:>s6A!(Q,AB1RNE'N<ϗ-Y>&Bi( CxxxHH0DQaLm׊lx4B!Ƈb `lݺm۶ (0g% `j2 B͚5ˆ$Bid( ʕ+YJBHSl;(a0 >>ކ$Bid( IG ,Jr*Vya[ Ô 8Іtz?kG\i ߟ>iP{-ԩߞW]:iw'cܙA![rog[:U>`Cg4kP#Gc֬YZ1cFJJʞ={۷o߾iӦ6oG/^~Ο?_.á ְr`޼y:=n6>}z?k%ojGM@!Y=+sX+/2,)!!...n+6oikJU>`Q˧JZUXѐk}mBXֻw7n0 c4bUNu:իW80owr<-aOm.|=miv2Pܐݜ_ wg.2ґB·k;*޹<sݼT^mۨ@^iZJigUo\ښ"AQJp%G HYuHwe}͛7oɽzѣGXXŋ;fqmۦT*"~ذ XןMHHՑF,w̴7)e7ukB켎P($aɘ@xT@qI.}ٱc I˦f[h|q 5|j%+,ۺ}kgM~V?[iN=q/-HhXYܑ4G9QZVoN@>ﱑCGmM|нWJvΞ4)) }'/i-f DŽ'OW-|< u*cOY϶Ww.XӽT?ktΝ/;vB}$H3n{,o|?]v?h"aFѮ]O>w9{7g,S+ڶx@\VѤ۶ Ri͵U/<nJww>[Χ}{O[~o Hn|! qS={XhgG,9QRš1h+%yOiJ=x^M /)3PSZ|rh'<+z=7V/-`ާ}ZK ߕ] STa˧ ޓo.p^'ܬv=冬|k8wCx]N}t7xШ>4+:ea^1~Kg-<ڷ3&\,}% -{ N18mz*=n+-rYiY9#ADS7iҤG|!ߎߝhgUp_L&Nb |Nj3~8e=c) 맔z @WS n<ϮXS z:^Oz΁Y@~Zж ~~fGбG!j> \ՃIȂ^ s#TxxæsWjxV&TGSzr -x 6.~?/"@Ahp^se2 x,P-yX4kx;V7)=Tro9%W7-!4K4iofRR0XN1$L`YV&I nnn<.]eYKxٳgGGG(ˍF#۰P 9s'mʪ_nCS jJb/5x{/}RQ/pYyO-r4noHU{/KKyNEU%~*wpdᅫWk,k 58ZK:[u¬SWFAÝK(@sA O< zD? ɳiV;&r-6d[ 5>xڪ2'Exht7*&\ w =2v,qǒ"VY),md WvO^4 m !Ė(_8q>eY֍ kN] Wv}giW*8rB Pjus?s,ݿ1:Z@7(~F鿖j{f]/޵yK^TݲX{Ӭ|LrC־]eI+%W|UV\CT*OnF htv,\WKs]>m/;UrdjdW,q&n]ED]5o-iGfӕ깑Bl% &@A L&4'M`;!A8h4/$ͼ}d23?????҂K{yzJOI>c)1:aA46zC[{[S3/ɓOw8Rewׯ4tTp!x^PC%1ۧz_ӹ˛=nqYޑ*F1xIIK#A<5N2%%yKr qҺ#'RIiUv!lWB0 Xx?n ǵZ=)Uk4ZnQ OMOVsv^ۺik]*dz򴴞{l{KiND{SjjOҽɟLKλJ̀-tiS.(9< J  {xf@@`PpHHHP`Hmj{F5j[‡UW|H]1AeǒCBBsKqW f Y|-P Ǭg,ǘNQg[h]ICgCD[o8 ET*V602 wƿ;r܂nҞRle񋦐!B^^޽sϩJ8C)yB.sIEei婈Uuiz=Gb/;@<}߾}"y@32:Զj Jχ*As=˷6oEENO}(-swҼӿ+[[)ѱI)۷̋|"i,5j[҇UQREI۷Dyhѹ ` ,KJH[eӖMxzws`ݴؐ Y:f=TùsunAsL&Fb ;;]vIII(Jw1EnnnXUd21 X`Y+L&&AEfUo窐*-w.T5i^'V&z9_YxH@y!![*7ZJ]q-3Cf/x>6;ⱘ+pbD*YȏǶQqcj k54YO.oß= Sz{s,x_)ۼRGezW4|P6^^/ | @}XX{F5jWt꽷Uç͝}\?pu}ns._4({ŧ@U7n-n4e< ,߲dR^oY ]@?;`I;d4G~ث_M݄tԣ97KN6C5s/AؖJ~gtv"?Ě_=#cmU??C%Ƽ{,**nn4.Cgg`hoo "N4R!IRKKAbyT9Zc.,r!p|r/AR})|UBJt=UfDz0ќkEC% IDATYZ6)āׁ+6w埥-NFĢ1¯Uv|6r]VaTZvA޴Oԅ7r]Y޷eIb8*7KWܛhjӃόuyF>ݿF)5{5 MLAߝYO)ΪCtTykGKeV{wDN:Ul=;M.]7Oh̎Ikuh-+ܴkU~y9'PNKk{xPj'4О.I/KNKsKUw>{I;}Cĵ{؛sdN ڽqME:qKx)WxVOh4HGvVѻI0ǧQ1ӕW^y+Wtz饗h9@GGokk_HƓ QBM_\ 7+Gsj2"nuf  dn6{mXp,vEPTp|ߑSq`  B ݟ+e,kSͶ7FLwDyb׭ Ota#8uP$Ecs^}˔*aɹ-*D?V KNz\ڔb˗˧۵0f?UVp5sŻ=(֧w׍|%_-COo E`7$0y-;*exe_H ܟ9XB \9gyڏ*bn;s hai'Ԇ҄aNSfҶ]?],gCV%S6G?ꀐ6={2'w͒sư̴yVI9e+Ü^8_wk} \ ?UeM 5O&}k#wJKz I~^%-Yd߾}$ $I=[ZZZD&[iʔ)oO:u=XֹsΛ7o~/CYVtEhKF; =N)ۺ6ݕ܀ۧm[ӚKC-x74oXza9cGTUnf6 rٚ5#,ઘw%5rR%rZT?V,DtRVNUH`jWÜy-} T?,? !O7䦒J,Ike%{SO+X:cݽLX*smVk3bغF;fKXkӛVݟ4q7^ɓ'"D~8.֑mmm^^^U`0tttL<׬Y"[o[oݰa7O=Tcc#z W2Κ5k3ғ1us[y Y [T@W_:h4w75݌C>E;y뫪-s㼘ư'Xm,Z}|[C 04߾J$iP'Z>,^nsPԳCqZ#`MrQ/ʋS>,pWnu#\j e,ӕz X"H7.`͙9y|bax.G d3 _@[3`yBߴ=%rJ̦Ԩ>8Y+lY*M$&iquAZW '{ݱ P+jtxbipط=yIi,zd3zltvw_= 㳱_~[+E 1V[l=Kz{{cǎq 0 ([¢\ ˏ'KvYA)9)?Z%x 蔜e H.N)034ǞS sBIՏ5"+yE e].R7L '|b#7-7ox/ +]֌>Of3fltMbPczN̊kdx߼=)U1KKDxփv9)*d2gՆl,#&<=JBWmi M>E]8MwZ?hsC/l|pQd]ԛ(*$=G^u/\ßgeeeb/Y\ ca/f"55ɓ$9G,hѢNtc ""c w ƍx 1qѵI&:::֮]_zr Xd+"pG gz"""" XZN1_cK.g[0۶m@Po1NCQ*K%;;;G;DDDDO"~iiiqsM`0̜97l/[ȑ#$h&Vmb MllhgB.ztt~+Л$ &4@c `ƍCH<3f8r-nx{{`1^y8vUމXb8qbѢEVĉ nU&螈?뮫/~1Tӟezp,g5FKE_,v_cM}͛7o2}^T{}M .Tjj=+U5Mc>ԭOeߊŋG2^1@kNJSTkZmUJtQ؝~'.V5)jCvm}Fzhs;cpyMlٲJJ>>>$\S *gߞKJ`q//wyn"-_?w !XxQ"0stٝLD7ts='{ )i*!%b:@;}Cĵ{؛s|"PWvڶ7oGQ4>"lR® sSWU.&kr5]&+W]cV)q)y7uhjj۳Ӕrܵ}S|u7f%o51rւ6g߭۱ݷ}WN)И@ޭ[ȀN ڽqME}^]YނonN,jPW[ɉ.F4{{ԩSbI|}}E|g CGGhA'`pjI;;;-Zt7Yeܒ%KEq 0Chy@w'GQ(]v=cf͚5=?+Wq#>l |oqBrr @ l_^n.޲>:qa^y yX5,9qF~hSފ-_09^%tΜusbFew9⾈e[e7uY00(@mgN T|:,lYzzSCgI_ ̇}[WcXfڼ +DDDbΝ;v8s?Xm_w[[x:d{:89zx ZQQQK.55u&-"i(D5 D &j[o.\o~ss:|LU% ۰"" c!e=O)V~uv ;mڴk .L=h6&k [F;#?,x"7s7 "> ;EqSA71 KLBcoO9'*@kGc4- Ag=@lF3K/Gv=gx[ItCr 9c0ϟ+I_|pˆ:|jbSOye:ٕLsa?m16jO^bFam{=0ACvX~#5<_5;!=f~/{vO<-8Vhp=Do4⥳!or `?QL8,MaY'̵#aAl}+"""0O?t{{{I<@'?IVV֐38?s VqX[ՈbEǜ"={^:w\z nμcGf2a*Q`L0Hu[bNYf(s0 I!yûB w X 񶍩s6L6H)*5~-e[`6`$ߩ0}r y.筺_:,%/sSdNۻhM޷#9nRrqbL1u0>DDD㕗iGя~! !zOu]p,+6ݤ,}S*o "˲]Mܿz "˲Ft3peߊŋG2v XxXbpn(x{{3E$-222233UUUUԩS/#GG`.yKe7"""#Wԛr4ћJ^<0(IL\s>PT^hT?19!_v^l:j]_}fώM\.5u>gٖ RKN{BRxGt>ݣGZk\otRF|ko{jd'ыccccGWG;5ZI"-oءHM:֧[ыcbccGvAwfߙd^PYpt5:&666&ښ_(vչNҽt-՞ؑϮ2mt3wiUыr}. Xñ׃lٲ+W|;::Z[[ѽZDDDxxW^(ĉ)SO8o팟u$ #W_"F#H|`_cYy*sVsSw@; o`WÜ]VѽzDp\zI]IɃcON+8e:k ey问7O]Rɦu#H~_ܗKLPkܰ;$~n_}?}{oܝC*_go߰C{Oo4[ p7o0F|Ę93/^ڐ6 ¤Ϸ~f_Ңijx4asax-wspԟ*+ez nB%2{1{rڳ=z6tr$f JDi.^2V9]bfΜoG _|+?HKK7y뭷vmc=p"}[b0d8>95>n/[(EdfZkwIt֐d[kZ;֋ ؟={^Ք/DP_mM*wir6hoZc7rk};j۹GU>^,?eNbniaήiw[V79n**j',k[$h_w5K;BΪ.jfO㗇ܠ;wKݿGު6_$*tOy~S$˲- [?hR72K_oY|)`T0ڐ@Kݑ&9^oEj&u'["*pTp9 8**mppU̽~b[RW+ӍډokR~EWWT$1ܹs,X0ڹ ǎ!~~~7#Kԗ\wHp_4@ _1y}xUѫ|4''#GVMJu 3abnꊣMv:{ug{ SQ9A` k%ߢ \U]P7mzE7u \xeoc-&`Y>f>=%09/kw9h^X Zs+g-":oJ~tJށ?0aPt`  5-_qF>ݿF)5{5 MLAߝYO)ΪCtTYkuy홶J/7K䭭Suj۳Ӕrܵ}S|ļ]n-ܒ(rM @ל8Z-- 7X7v:r 5?u",]_ZdN^BuK|pJejF[ts9Y[]|CKڑ&{ޜ'OWmu@vZk*׉[Lyó~BC~Ab'XCC`kn C6}qr%KRܜ6Εˈbd  dn64߂Z |huhBWo8g;ZC"|(*8?OWȩa0HA!啂Js|v5]O]61]c^ @ ^6{7[[R-%ô971,3m^@RN0-{sWhEw=zMӛjN>#M: Gcڗ ?T/lM&1Õ%ꔂka] }ڶ5?Ԃw? 9?@|UJzW3vDEk@5kVGZw%Fg:h]?g/{֢T`!œ>):NUH`j3ʞkĸOki(b}y׍x%7VbMLZĬ?^~`2+Ir9y2^ڪP5Aϧ~u[VH-:؛EC{ S[75m+9~xɮ0uOW9ݍ@Sŏ3 v~Bcekl7^uZ[>, piWWwB_w݄K `mw0?пmtϩwxw&OU"3 vAlF3Khk,?Xh9*tX(Wۨl*J꓌Fg˂%Wi$i5I+ ךO_~ 9jEGNc zTퟐnL=Cm:wOmnpa<=VtRKaqͿؘ跾Ya %f6k2} t?: yBΜW 4{α>Ơ )S\8wwF7&LB*{SS oaY/5@?+Zc\;bYQ̀ԤMb-ݿ-mTU3]nأ7o|?/fwכQxj^S^v/5躲b9~iVb&{U'CnZR |A}C׬m7oeiT>G}y׍d%ǟsa: 'MWmǬ+/^m;ޥ,sB 7I3ȍUG{U?\ooƊ+Aw#ڌj}:{}}mm}^癊ƦƆ0M3jѣvYx]MWS7!'?T\^^|: HD,HO$2msNU׬ԭ8 'z$-t6JS.g>jtxbipط=yIi,zd3zltvw_= 3rnnn y- yȅ¿U(,Pxr-dDu[bNYf Ęb![ 3JIj쎮Is 10'XxZ/WdR"uÄ}ԧSLs %`gUI&liǓ`.Y pB|ު;\_!Y[mSv4cֽ|YKL-J,%!`uFO?]7b `b97J;!f7Ĥ/ 8$_~omqT|EeJ'%/sSMlNۻ|{|~ѹvg!&%;&AgRrޘhsJքuιy]emS $@欮gy[ -w5|;I@ )`wmWgQDKz6:OJw~a̭؃y=kǎf2AwdDۑ `A14sWʚ-qŰuSܴ W9㥪/I^NNƍ2"Iiw}pdb{ ʺ#ʲl@-^tWʫ5Y51Y$4Q5nsҮ.vR'f]֢ԦFeM&'h,Y/'CO{ly@7A4}r_ QdUFM(_'4YV&UEQ5M2NݍnE4TEV5^tjnBNJMwom“[9P>[-:*KBw*˪/{VSV=M;DgΜ9L'7K+111ރ2m2>aƝXO'*6&uIvɚz3'TEL,ڰ󁍅vdGI認=<=砨'_ Nt}n}腍]7 QzSEGȫG}hqŌ;Gl 5c w0 WFha܁#7K D)soiM'^xM6_9c!MUTHB`䓕;QjE{sjcqxk3o\C;KS_{R:vXddPg"5Ŏ;89U\\$N}|||Ho%ȩM:::D`0x{{wvvz{{ v'?HeF^zٳ^^^ގ6$yyypc͚5#Y""""l }qHSimmm"`rItTUU=ha,A^z%J#&L8q׮]#KrJwyPbOƮ ߳R[U4>xЭdeߊŋG2#fZct7UXϤǦ:{T)ڐd[kZ;֋<ψ1)sdؙݫT2Whc,A};wNu-R bD&DÅϺun-3^"R e=p`oNfΉ'@]i޼ks}GȊԲI d&LM]qTl\wɑv\uI'u~SSY5 Գo>o@k5՚2g)kn42 Kޒ@kcnw埥-NFĢP?=c\}ׁiA7pV̯[g//|W-WW[.ADDc kb&1 XDسg~GF4")"nu\f͝Y `DF~{!#E?Vd>:4zC4p59rgկZ4 yX5,N ^&_09^%tΜusϓ+jۨ$E0B*azŔ &`KDϓw`Zng0\jI%ai7Z5(hb,A} : fH$L:u߾}#U$)\[bQGK&O1vhn4,> V ;n]?<¬$Mv0_"^h竸BitY8܂kF{֢T`!œJ(8FNci%SpsଘkeUKDxBN^6hq:eÒ;::Exyy0C2 ?̙3G'4aqͿؘ跾Yxq Kn2cc?./P+ +-_|2HC=QTZcoLtfW2ϝy˚Vx1p3딘x[yACŧv Y6]n*USu3UOIEוKC}SwV̉@?+Zc\;bIS.g>jtx]YrZ@\Vȁ'O~:DVoovμcGf2a*Q`L0Hu[bNYf(s0 I!yؾ1Z0EdmmL]۵aALQ7o-$NW+VE4O(8w!P>o=_L\S $~af#)v$Y/n-9'xWv#""?TQk pvaa"{ɩ4UQTM2MTYV`2蠩J^4EV4lv:5fO̺E% j*[Lƶx]e+dh6UxUUjkkse̞MDDD"rKKb\o]׿~mmmaCM}_C IDAT˲ ""a!>mڴu/v?<//6sꪫ>>8qUW];ybg4}||xyy9 'xQtQ۹sW{{ꈈ`E{;;;Ea0}BDDDD}]^^^21ڙ""""1kƍc,*t:th$DDDDDcc+rq///I8z:<#"""1%.RwuÇ~~~7""""1]v>|upҒ?#"""1iӦ={V&!%\$,]gLg NIDĀ38utt={vD2HDDDDccˑ#G{96H ѻŁ/\X>Nkn{yyyyy@Q_\FvHZZG}6F.DDDD4]bQYYhѢI4MsW{{7ܑ+%.7tɓ'}}}[[[ϯELSO@hlay///Hu ---:;;H.K\M.GEDDī:r$"""1_ZZٳg}||D`0\Z[[cccH l&NiZϩ`Mw?>b9$"""?!&nDLM~@vq_UUKBDbf؞:YVZn3LDDDDc%Ƴ~HO,%?! """"r%ƳWUU8!Z'}||fΜo|&hbĸUSSo"xpSnoo;3 """Aa,1n(g&ǧ?9Z$"""14{ccݻW\9#"""%-cϩLRZZz7tΈh\`x&z7YNf_9sf0㟷w{{;f0***a{=nEGGwvvz{{YSB~'&M$Z$Hp}nh|`,1۷ٳ.]~%/ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDD`,ADDDDDl:N W+IENDB`circus-0.12.1/docs/source/index.rst000066400000000000000000000054711256046442300171710ustar00rootroot00000000000000Circus: A Process & Socket Manager ################################## .. image:: circus-medium.png :align: right Circus is a Python program which can be used to monitor and control processes and sockets. Circus can be driven via a command-line interface, a web interface or programmatically through its python API. To install it and try its features check out the :ref:`examples`, or read the rest of this page for a quick introduction. Running a Circus Daemon ----------------------- Circus provides a command-line script call **circusd** that can be used to manage :term:`processes` organized in one or more :term:`watchers`. Circus' command-line tool is configurable using an ini-style configuration file. Here's a very minimal example: .. code-block:: ini [watcher:program] cmd = python myprogram.py numprocesses = 5 [watcher:anotherprogram] cmd = another_program numprocesses = 2 The file is then passed to *circusd*:: $ circusd example.ini Besides processes, Circus can also bind sockets. Since every process managed by Circus is a child of the main Circus daemon, that means any program that's controlled by Circus can use those sockets. Running a socket is as simple as adding a *socket* section in the config file: .. code-block:: ini [socket:mysocket] host = localhost port = 8080 To learn more about sockets, see :ref:`sockets`. To understand why it's a killer feature, read :ref:`whycircussockets`. Controlling Circus ------------------ Circus provides two command-line tools to manage your running daemon: - *circusctl*, a management console you can use to perform actions such as adding or removing :term:`workers` - *circus-top*, a top-like console you can use to display the memory and cpu usage of your running Circus. To learn more about these, see :ref:`cli` Circus also offers a web dashboard that can connect to a running Circus daemon and let you monitor and interact with it. To learn more about this feature, see :ref:`circushttpd` What now ? ========== If you are a developer and want to leverage Circus in your own project, write plugins or hooks, go to :ref:`fordevs`. If you are an ops and want to manage your processes using Circus, go to :ref:`forops`. Contributions and Feedback ========================== More on contributing: :ref:`contribs`. Useful Links: - There's a mailing-list for any feedback or question: http://tech.groups.yahoo.com/group/circus-dev/ - The repository and issue tracker are on GitHub : https://github.com/circus-tent/circus - Join us on the IRC : Freenode, channel **#circus-tent** Documentation index =================== .. toctree:: :maxdepth: 2 installation tutorial/index for-ops/index for-devs/index usecases design/index contributing faq changelog man/index glossary copyright circus-0.12.1/docs/source/installation.rst000066400000000000000000000032611256046442300205560ustar00rootroot00000000000000.. _installation: Installing Circus ################# Circus is a Python package which is published on PyPI - the Python Package Index. The simplest way to install it is to use pip, a tool for installing and managing Python packages:: $ pip install circus Or download the `archive on PyPI `_, extract and install it manually with:: $ python setup.py install If you want to try out Circus, see the :ref:`examples`. If you are using debian or any debian based distribution, you also can use the ppa to install circus, it's at https://launchpad.net/~roman-imankulov/+archive/circus zc.buildout =========== We provide a `zc.buildout `_ configuration, you can use it by simply running the bootstrap script, then calling buildout:: $ python bootstrap.py $ bin/buildout More on Requirements ==================== Circus works with: - Python 2.6, 2.7, 3.2 or 3.3 - zeromq >= 2.1.10 - The version of zeromq supported is ultimately determined by what version of `pyzmq `_ is installed by pip during circus installation. - Their current release supports 2.x (limited), 3.x, and 4.x ZeroMQ versions. - **Note**: If you are using PyPy instead of CPython, make sure to read their installation docs as ZeroMQ version support is not the same on PyPy. When you install circus, the latest versions of the Python dependencies will be pulled out for you. You can also install them manually using the pip-requirements.txt file we provide:: $ pip install -r pip-requirements.txt If you want to run the Web console you will need to install **circus-web**:: $ pip install circus-web circus-0.12.1/docs/source/man/000077500000000000000000000000001256046442300160745ustar00rootroot00000000000000circus-0.12.1/docs/source/man/circus-plugin.rst000066400000000000000000000022261256046442300214140ustar00rootroot00000000000000circus-plugin man page ###################### Synopsis -------- circus-plugin [options] [plugin] Description ----------- circus-plugin allows to launch a plugin from a running Circus daemon. Arguments --------- :plugin: Fully qualified name of the plugin class. Options ------- :--endpoint *ENDPOINT*: Connection endpoint. :--pubsub *PUBSUB*: The circusd ZeroMQ pub/sub socket to connect to. :--config *CONFIG*: The plugin configuration file. :--check-delay *CHECK_DELAY*: Check delay. :\--log-level *LEVEL*: Specify the log level. *LEVEL* can be `info`, `debug`, `critical`, `warning` or `error`. :\--log-output *LOGOUTPUT*: The location where the logs will be written. The default behavior is to write to stdout (you can force it by passing '-' to this option). Takes a filename otherwise. :--ssh *SSH*: SSH Server in the format ``user@host:port``. :-h, \--help: Show the help message and exit. :\--version: Displays Circus version and exits. See also -------- `circus` (1), `circusd` (1), `circusctl` (1), `circusd-stats` (1), `circus-top` (1). Full Documentation is available at http://circus.readthedocs.org circus-0.12.1/docs/source/man/circus-top.rst000066400000000000000000000013111256046442300207120ustar00rootroot00000000000000circus-top man page ################### Synopsis -------- circus-top [options] Description ----------- circus-top is a *top*-like command to display the Circus daemon and processes managed by circus. Options ------- :--endpoint *ENDPOINT*: Connection endpoint. :--ssh *SSH*: SSH Server in the format ``user@host:port``. :--process-timeout *PROCESS_TIMEOUT*: After this delay of inactivity, a process will be removed. :-h, \--help: Show the help message and exit. :\--version: Displays Circus version and exits. See also -------- `circus` (1), `circusctl` (1), `circusd` (1), `circusd-stats` (1), `circus-plugin` (1). Full Documentation is available at http://circus.readthedocs.org circus-0.12.1/docs/source/man/circusctl.rst000066400000000000000000000034231256046442300206230ustar00rootroot00000000000000circusctl man page ################## Synopsis -------- circusctl [options] command [args] Description ----------- circusctl is front end to control the Circus daemon. It is designed to help the administrator control the functionning of the Circud **circusd** daemon. Commands -------- :add: Add a watcher :decr: Decrement the number of processes in a watcher :dstats: Get circusd stats :get: Get the value of specific watcher options :globaloptions: Get the arbiter options :incr: Increment the number of processes in a watcher :ipython: Create shell into circusd process :list: Get list of watchers or processes in a watcher :listen: Subscribe to a watcher event :listsockets: Get the list of sockets :numprocesses: Get the number of processes :numwatchers: Get the number of watchers :options: Get the value of all options for a watcher :quit: Quit the arbiter immediately :reload: Reload the arbiter or a watcher :reloadconfig: Reload the configuration file :restart: Restart the arbiter or a watcher :rm: Remove a watcher :set: Set a watcher option :signal: Send a signal :start: Start the arbiter or a watcher :stats: Get process infos :status: Get the status of a watcher or all watchers :stop: Stop watchers Options ------- :--endpoint *ENDPOINT*: connection endpoint :-h, \--help: Show the help message and exit :--json: output to JSON :--prettify: prettify output :--ssh *SSH*: SSH Server in the format ``user@host:port`` :--ssh_keyfile *SSH_KEYFILE*: path to the keyfile to authorise the user :--timeout *TIMEOUT*: connection timeout :\--version: Displays Circus version and exits. See Also -------- `circus` (1), `circusd` (1), `circusd-stats` (1), `circus-plugin` (1), `circus-top` (1). Full Documentation is available at http://circus.readthedocs.org circus-0.12.1/docs/source/man/circusd-stats.rst000066400000000000000000000020241256046442300214140ustar00rootroot00000000000000circusd-stats man page ###################### Synopsis -------- circusd-stats [options] Description ----------- circusd-stats runs the stats aggregator for Circus. Options ------- :--endpoint *ENDPOINT*: Connection endpoint. :--pubsub *PUBSUB*: The circusd ZeroMQ pub/sub socket to connect to. :--statspoint *STATSPOINT*: The ZeroMQ pub/sub socket to send data to. :\--log-level *LEVEL*: Specify the log level. *LEVEL* can be `info`, `debug`, `critical`, `warning` or `error`. :\--log-output *LOGOUTPUT*: The location where the logs will be written. The default behavior is to write to stdout (you can force it by passing '-' to this option). Takes a filename otherwise. :--ssh *SSH*: SSH Server in the format ``user@host:port``. :-h, \--help: Show the help message and exit. :\--version: Displays Circus version and exits. See also -------- `circus` (1), `circusd` (1), `circusctl` (1), `circus-plugin` (1), `circus-top` (1). Full Documentation is available at http://circus.readthedocs.org circus-0.12.1/docs/source/man/circusd.rst000066400000000000000000000023661256046442300202710ustar00rootroot00000000000000circusd man page ################ Synopsis -------- circusd [options] [config] Description ----------- circusd is the main process of the Circus architecture. It takes care of running all the processes. Each process managed by Circus is a child process of **circusd**. Arguments --------- :config: configuration file Options ------- :-h, \--help: Show the help message and exit :\--log-level *LEVEL*: Specify the log level. *LEVEL* can be `info`, `debug`, `critical`, `warning` or `error`. :\--log-output *LOGOUTPUT*: The location where the logs will be written. The default behavior is to write to stdout (you can force it by passing '-' to this option). Takes a filename otherwise. :\--logger-config *LOGGERCONFIG*: The location where a standard Python logger configuration INI, JSON or YAML file can be found. This can be used to override the default logging configuration for the arbiter. :\--daemon: Start circusd in the background. :\--pidfile *PIDFILE*: The location of the PID file. :\--version: Displays Circus version and exits. See also -------- `circus` (1), `circusctl` (1), `circusd-stats` (1), `circus-plugin` (1), `circus-top` (1). Full Documentation is available at http://circus.readthedocs.org circus-0.12.1/docs/source/man/index.rst000066400000000000000000000001731256046442300177360ustar00rootroot00000000000000man pages ######### .. toctree:: :maxdepth: 1 circusd circusctl circus-plugin circus-top circusd-stats circus-0.12.1/docs/source/tutorial/000077500000000000000000000000001256046442300171645ustar00rootroot00000000000000circus-0.12.1/docs/source/tutorial/index.rst000066400000000000000000000001161256046442300210230ustar00rootroot00000000000000Tutorial ######## .. toctree:: :maxdepth: 2 step-by-step rationale circus-0.12.1/docs/source/tutorial/rationale.rst000066400000000000000000000113571256046442300217030ustar00rootroot00000000000000.. _why: Why should I use Circus instead of X ? ###################################### 1. **Circus simplifies your web stack process management** Circus knows how to manage processes *and* sockets, so you don't have to delegate web workers management to a WGSI server. See :ref:`whycircussockets` 2. **Circus provides pub/sub and poll notifications via ZeroMQ** Circus has a :term:`pub/sub` channel you can subscribe to. This channel receives all events happening in Circus. For example, you can be notified when a process is :term:`flapping`, or build a client that triggers a warning when some processes are eating all the CPU or RAM. These events are sent via a ZeroMQ channel, which makes it different from the stdin stream Supervisord uses: - Circus sends events in a fire-and-forget fashion, so there's no need to manually loop through *all* listeners and maintain their states. - Subscribers can be located on a remote host. Circus also provides ways to get status updates via one-time polls on a req/rep channel. This means you can get your information without having to subscribe to a stream. The :ref:`cli` command provided by Circus uses this channel. See :ref:`examples`. 3. **Circus is (Python) developer friendly** While Circus can be driven entirely by a config file and the *circusctl* / *circusd* commands, it is easy to reuse all or part of the system to build your own custom process watcher in Python. Every layer of the system is isolated, so you can reuse independently: - the process wrapper (:class:`Process`) - the processes manager (:class:`Watcher`) - the global manager that runs several processes managers (:class:`Arbiter`) - and so on… 4. **Circus scales** One of the use cases of Circus is to manage thousands of processes without adding overhead -- we're dedicated to focusing on this. .. _supervisor: Coming from Supervisor ====================== Supervisor is a very popular solution in the Python world and we're often asked how Circus compares with it. If you are coming from `Supervisor `_, this page tries to give an overview of how the tools differ. Differences overview -------------------- Supervisor & Circus have the same goals - they both manage processes and provide a command-line script — respectively **supervisord** and **circusd** — that reads a configuration file, forks new processes and keep them alive. Circus has an extra feature: the ability to bind sockets and let the processes it manages use them. This "pre-fork" model is used by many web servers out there, like `Apache `_ or `Unicorn `_. Having this option in Circus can simplify a web app stack: all processes and sockets are managed by a single tool. Both projects provide a way to control a running daemon via another script. respectively **supervisorctl** and **circusctl**. They also both have events and a way to subscribe to them. The main difference is the underlying technology: Supervisor uses XML-RPC for interacting with the daemon, while Circus uses ZeroMQ. Circus & Supervisor both have a web interface to display what's going on. Circus' is more advanced because you can follow in real time what's going on and interact with the daemon. It uses web sockets and is developed in a separate project (`circus-web `_.) There are many other subtle differences in the core design, we might list here one day… In the meantime, you can learn more about circus internals in :ref:`design`. Configuration ------------- Both systems use an ini-like file as a configuration. - `Supervisor documentation `_ - `Circus documentation `_ Here's a small example of running an application with Supervisor. In this case, the application will be started and restarted in case it crashes :: [program:example] command=npm start directory=/home/www/my-server/ user=www-data autostart=true autorestart=true redirect_stderr=True In Circus, the same configuration is done by:: [watcher:example] cmd=npm start working_dir=/home/www/my-server/ user=www-data stderr_stream.class=StdoutStream Notice that the stderr redirection is slightly different in Circus. The tool does not have a **tail** feature like in Supervisor, but will let you hook any piece of code to deal with the incoming stream. You can create your own stream hook (as a Class) and do whatever you want with the incoming stream. Circus provides some built-in stream classes like **StdoutStream**, **FileStream**, **WatchedFileStream**, or **TimedRotatingFileStream**. .. XXX add more complex examples circus-0.12.1/docs/source/tutorial/step-by-step.rst000066400000000000000000000071741256046442300222630ustar00rootroot00000000000000.. _examples: Step-by-step tutorial ##################### The `examples directory `_ in the Circus repository contains many examples to get you started, but here's a full tutorial that gives you an overview of the features. We're going to supervise a WSGI application. Installation ------------ Circus is tested on Mac OS X and Linux with the latest Python 2.6, 2.7, 3.2 and 3.3. To run a full Circus, you will also need **libzmq**, **libevent** & **virtualenv**. On Debian-based systems:: $ sudo apt-get install libzmq-dev libevent-dev python-dev python-virtualenv Create a virtualenv and install *circus*, *circus-web* and *chaussette* in it :: $ virtualenv /tmp/circus $ cd /tmp/circus $ bin/pip install circus $ bin/pip install circus-web $ bin/pip install chaussette Once this is done, you'll find a plethora of commands in the local bin dir. Usage ----- *Chaussette* comes with a default Hello world app, try to run it:: $ bin/chaussette You should be able to visit http://localhost:8080 and see *hello world*. Stop Chaussette and add a circus.ini file in the directory containing: .. code-block:: ini [circus] statsd = 1 httpd = 1 [watcher:webapp] cmd = bin/chaussette --fd $(circus.sockets.web) numprocesses = 3 use_sockets = True [socket:web] host = 127.0.0.1 port = 9999 This config file tells Circus to bind a socket on port *9999* and run 3 chaussettes workers against it. It also activates the Circus web dashboard and the statistics module. Save it & run it using **circusd**:: $ bin/circusd --daemon circus.ini Now visit http://127.0.0.1:9999, you should see the hello world app. The difference now is that the socket is managed by Circus and there are several web workers that are accepting connections against it. .. note:: The load balancing is operated by the operating system so you're getting the same speed as any other pre-fork web server like Apache or NGinx. Circus does not interfer with the data that goes through. You can also visit http://localhost:8080/ and enjoy the Circus web dashboard. Interaction ----------- Let's use the circusctl shell while the system is running:: $ bin/circusctl circusctl 0.7.1 circusd-stats: active circushttpd: active webapp: active (circusctl) You get into an interactive shell. Type **help** to get all commands:: (circusctl) help Documented commands (type help ): ======================================== add get list numprocesses quit rm start stop decr globaloptions listen numwatchers reload set stats dstats incr listsockets options restart signal status Undocumented commands: ====================== EOF help Let's try basic things. Let's list the web workers processes and add a new one:: (circusctl) list webapp 13712,13713,13714 (circusctl) incr webapp 4 (circusctl) list webapp 13712,13713,13714,13973 Congrats, you've interacted with your Circus! Get off the shell with Ctrl+D and now run circus-top:: $ bin/circus-top This is a top-like command to watch all your processes' memory and CPU usage in real time. Hit Ctrl+C and now let's quit Circus completely via circus-ctl:: $ bin/circusctl quit ok Next steps ---------- You can plug your own WSGI application instead of Chaussette's hello world simply by pointing the application callable. Chaussette also comes with many backends like Gevent or Meinheld. Read https://chaussette.readthedocs.org/ for all options. circus-0.12.1/docs/source/usecases.rst000066400000000000000000000066711256046442300177000ustar00rootroot00000000000000Use cases examples ################## This chapter presents a few use cases, to give you an idea on how to use Circus in your environment. Running a WSGI application ========================== Running a WSGI application with Circus is quite interesting because you can watch & manage your *web workers* using *circus-top*, *circusctl* or the Web interface. This is made possible by using Circus sockets. See :ref:`whycircussockets`. Let's take an example with a minimal `Pyramid `_ application:: from pyramid.config import Configurator from pyramid.response import Response def hello_world(request): return Response('Hello %(name)s!' % request.matchdict) config = Configurator() config.add_route('hello', '/hello/{name}') config.add_view(hello_world, route_name='hello') application = config.make_wsgi_app() Save this script into an **app.py** file, then install those projects:: $ pip install Pyramid $ pip install chaussette Next, make sure you can run your Pyramid application using the **chaussette** console script:: $ chaussette app.application Application is Serving on localhost:8080 Using as a backend And check that you can reach it by visiting **http://localhost:8080/hello/tarek** Now that your application is up and running, let's create a Circus configuration file: .. code-block:: ini [circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:webworker] cmd = chaussette --fd $(circus.sockets.webapp) app.application use_sockets = True numprocesses = 3 [socket:webapp] host = 127.0.0.1 port = 8080 This file tells Circus to bind a socket on port *8080* and run *chaussette* workers on that socket -- by passing its fd. Save it to *server.ini* and try to run it using **circusd** :: $ circusd server.ini [INFO] Starting master on pid 8971 [INFO] sockets started [INFO] circusd-stats started [INFO] webapp started [INFO] Arbiter now waiting for commands Make sure you still get the app on **http://localhost:8080/hello/tarek**. Congrats ! you have a WSGI application running 3 workers. You can run the :ref:`circushttpd` or the :ref:`cli`, and enjoy Circus management. Running a Django application ============================ Running a Django application is done exactly like running a WSGI application. Use the *PYTHONPATH* to import the directory the project is in, the directory that contains the directory that has settings.py in it (with Django 1.4+ this directory has manage.py in it) : .. code-block:: ini [socket:dwebapp] host = 127.0.0.1 port = 8080 [watcher:dwebworker] cmd = chaussette --fd $(circus.sockets.dwebapp) dproject.wsgi.application use_sockets = True numprocesses = 2 [env:dwebworker] PYTHONPATH = /path/to/parent-of-dproject If you need to pass the *DJANGO_SETTINGS_MODULE* for a backend worker for example, you can pass that also though the *env* configation option: .. code-block:: ini [watcher:dbackend] cmd = /path/to/script.py numprocesses=3 [env:dbackend] PYTHONPATH = /path/to/parent-of-dproject DJANGO_SETTINGS_MODULE=dproject.settings See http://chaussette.readthedocs.org for more about chaussette. circus-0.12.1/examples/000077500000000000000000000000001256046442300147075ustar00rootroot00000000000000circus-0.12.1/examples/__init__.py000066400000000000000000000000001256046442300170060ustar00rootroot00000000000000circus-0.12.1/examples/addworkers.py000066400000000000000000000012511256046442300174250ustar00rootroot00000000000000from circus.client import CircusClient from circus.util import DEFAULT_ENDPOINT_DEALER client = CircusClient(endpoint=DEFAULT_ENDPOINT_DEALER) command = '../bin/python dummy_fly.py 111' name = 'dummy' for i in range(50): print(client.call(""" { "command": "add", "properties": { "cmd": "%s", "name": "%s", "options": { "copy_env": true, "stdout_stream": { "filename": "stdout.log" }, "stderr_stream": { "filename": "stderr.log" } }, "start": true } } """ % (command, name + str(i)))) circus-0.12.1/examples/apis.py000066400000000000000000000003031256046442300162110ustar00rootroot00000000000000 from circus import get_arbiter myprogram = {"cmd": "sleep 30", "numprocesses": 4} print('Runnning...') arbiter = get_arbiter([myprogram]) try: arbiter.start() finally: arbiter.stop() circus-0.12.1/examples/byapi.py000066400000000000000000000003571256046442300163720ustar00rootroot00000000000000myprogram = { "cmd": "python", "args": "-u dummy_fly.py $(circus.wid)", "numprocesses": 3, } from circus import get_arbiter arbiter = get_arbiter([myprogram], debug=True) try: arbiter.start() finally: arbiter.stop() circus-0.12.1/examples/demo.py000066400000000000000000000002021256046442300161770ustar00rootroot00000000000000import random def set_var(watcher, arbiter, hook_name): watcher.env['myvar'] = str(random.randint(10, 100)) return True circus-0.12.1/examples/dummy_fly.py000077500000000000000000000014021256046442300172660ustar00rootroot00000000000000#!/usr/bin/env python import os import signal import sys class DummyFly(object): def __init__(self, wid): self.wid = wid # init signal handling signal.signal(signal.SIGQUIT, self.handle_quit) signal.signal(signal.SIGTERM, self.handle_quit) signal.signal(signal.SIGINT, self.handle_quit) signal.signal(signal.SIGCHLD, self.handle_chld) self.alive = True def handle_quit(self, *args): self.alive = False sys.exit(0) def handle_chld(self, *args): return def run(self): print("hello, fly #%s (pid: %s) is alive" % (self.wid, os.getpid())) a = 2 while self.alive: a = a + 200 if __name__ == "__main__": DummyFly(sys.argv[1]).run() circus-0.12.1/examples/dummy_fly2.py000066400000000000000000000013601256046442300173500ustar00rootroot00000000000000import os import signal import sys import time class DummyFly(object): def __init__(self, wid): self.wid = wid # init signal handling signal.signal(signal.SIGQUIT, self.handle_quit) signal.signal(signal.SIGTERM, self.handle_quit) signal.signal(signal.SIGINT, self.handle_quit) signal.signal(signal.SIGCHLD, self.handle_chld) self.alive = True def handle_quit(self, *args): self.alive = False sys.exit(0) def handle_chld(self, *args): return def run(self): print("hello, fly 2 #%s (pid: %s) is alive" % (self.wid, os.getpid())) while self.alive: time.sleep(0.1) if __name__ == "__main__": DummyFly(sys.argv[1]).run() circus-0.12.1/examples/example1.ini000066400000000000000000000007471256046442300171340ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 httpd = True debug = True [watcher:dummy] cmd = python args = -u dummy_fly.py $(circus.wid) warmup_delay = 0 numprocesses = 5 [watcher:dummy2] cmd = python args = -u dummy_fly2.py $(circus.wid) warmup_delay = 0 numprocesses = 3 rlimit_nofile = 300 rlimit_nproc = 10 [plugin:flapping] use = circus.plugins.flapping.Flapping retry_in = 3 max_retry = 2 circus-0.12.1/examples/example10.ini000066400000000000000000000002741256046442300172070ustar00rootroot00000000000000[circus] debug = 1 [watcher:vars] cmd = echo $(circus.env.myvar); sleep 1 hooks.before_spawn = examples.demo.set_var stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream circus-0.12.1/examples/example2.ini000066400000000000000000000010211256046442300171170ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 ;stream_backend = gevent [watcher:verbose] cmd = python args = -u verbose_fly.py warmup_delay = 0 numprocesses = 10 ; stream options stdout_stream.class = FileStream stdout_stream.filename = test.log stdout_stream.refresh_time = 0.3 [watcher:verbose2] cmd = python args = -u verbose_fly.py numprocesses = 4 [plugin:flapping] use = circus.plugins.flapping.Flapping retry_in = 3 max_retry = 2 circus-0.12.1/examples/example3.ini000066400000000000000000000006411256046442300171270ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 [watcher:dying] cmd = bash test.sh warmup_delay = 0 numprocesses = 1 flapping.active = False [watcher:dying2] cmd = bash test.sh warmup_delay = 0 numprocesses = 1 flapping.retry_in = 1 flapping.max_retry = 2 flapping.active = True [plugin:flapping] use = circus.plugins.flapping.Flapping retry_in = 3 max_retry = 2 circus-0.12.1/examples/example4.ini000066400000000000000000000007561256046442300171370ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:dummy] cmd = python args = -u dummy_fly.py $WID warmup_delay = 0 numprocesses = 5 [watcher:dummy2] cmd = python args = -u dummy_fly2.py $WID warmup_delay = 0 numprocesses = 3 rlimit_nofile = 300 rlimit_nproc = 10 [plugin:statsd] use = circus.plugins._statsd.StatsdEmitter host = localhost port = 8125 sample_rate = 1.0 application_name = example4 circus-0.12.1/examples/example5.ini000066400000000000000000000012701256046442300171300ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 httpd = 1 httpd_host = 127.0.0.1 httpd_port = 8080 debug = 1 [watcher:webworker] ; chaussette is an external project. If you want to test this file, you'll need ; to install it and point this example to where it's been installed. cmd = ../bin/chaussette --fd $(circus.sockets.webapp) --pre-hook chaussette.util.setup_bench --post-hook chaussette.util.teardown_bench chaussette.util.bench_app use_sockets = True warmup_delay = 0 numprocesses = 3 stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream [socket:webapp] host = 127.0.0.1 port = 8888 circus-0.12.1/examples/example6.ini000066400000000000000000000005501256046442300171310ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 httpd = True debug = True httpd_port = 8080 [watcher:swiss] cmd = ../bin/python args = -u flask_app.py warmup_delay = 0 numprocesses = 1 singleton = True stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream circus-0.12.1/examples/example7.ini000066400000000000000000000006561256046442300171410ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:webworker] cmd = chaussette --fd $(circus.sockets.webapp) flask_app.app use_sockets = True copy_path = True copy_env = True warmup_delay = 0 numprocesses = 1 stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream [socket:webapp] path = /tmp/webapp.sock family = AF_UNIX circus-0.12.1/examples/example8.ini000066400000000000000000000002061256046442300171310ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 circus-0.12.1/examples/example9.ini000066400000000000000000000005121256046442300171320ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 ;debug = 1 [watcher:leaker] cmd = python leaker.py warmup_delay = 0 numprocesses = 1 [plugin:leak] use = circus.plugins.resource_watcher.ResourceWatcher max_mem = 1 max_cpu = 4 service = leaker circus-0.12.1/examples/file.pdf000066400000000000000000004413571256046442300163370ustar00rootroot00000000000000%PDF-1.4 % 5 0 obj << /S /GoTo /D (section.1) >> endobj 8 0 obj (\311tude de la variable continue) endobj 9 0 obj << /S /GoTo /D (subsection.1.1) >> endobj 12 0 obj (Tableau statistique) endobj 13 0 obj << /S /GoTo /D (subsection.1.2) >> endobj 16 0 obj (Repr\351sentation Graphique) endobj 17 0 obj << /S /GoTo /D (subsection.1.3) >> endobj 20 0 obj (Repr\351sentation Num\351rique) endobj 21 0 obj << /S /GoTo /D (subsubsection.1.3.1) >> endobj 24 0 obj (Param\350tres \340 tendance centrale) endobj 25 0 obj << /S /GoTo /D (subsubsection.1.3.2) >> endobj 28 0 obj (Param\350tres de dispersion) endobj 29 0 obj << /S /GoTo /D (subsubsection.1.3.3) >> endobj 32 0 obj (Param\350tre de forme) endobj 33 0 obj << /S /GoTo /D [34 0 R /Fit ] >> endobj 37 0 obj << /Length 1882 /Filter /FlateDecode >> stream xڭr6@ n2C-hwmv2BWrD*M/t(ݍIS$νA}C_OW2pJ檹p#Uъ+ds}ܴt]d\m:RPq|Ypզ?͛aY- >[J+׭Pld_/i16K2R¨WqOǡ W~|P"m I%a_m_9WgIS+$: 92jt%ׄ[3yyLw(pМB0@/gO*7cLRWȷ{X>HЏ s [ʕ3 -~ˤ_J+=_ 'y:3oBKqPr 5ǟȈV}<5#SMlwUg aO8z%?ԮR&fЧ7lc}KPV(޶ ;ǯ#it: t BV/%l 6nVa,O$] EU>ǝ~lߧFm9Y{Vwкdx#ĥn(_^ endstream endobj 34 0 obj << /Type /Page /Contents 37 0 R /Resources 36 0 R /MediaBox [0 0 595.276 841.89] /Parent 49 0 R >> endobj 38 0 obj << /D [34 0 R /XYZ 102.884 738.009 null] >> endobj 39 0 obj << /D [34 0 R /XYZ 102.884 713.103 null] >> endobj 6 0 obj << /D [34 0 R /XYZ 102.884 520.437 null] >> endobj 10 0 obj << /D [34 0 R /XYZ 102.884 484.326 null] >> endobj 47 0 obj << /D [34 0 R /XYZ 212.019 251.883 null] >> endobj 36 0 obj << /Font << /F19 40 0 R /F20 41 0 R /F36 42 0 R /F37 43 0 R /F58 44 0 R /F16 45 0 R /F59 46 0 R /F60 48 0 R >> /ProcSet [ /PDF /Text ] >> endobj 52 0 obj << /Length 695 /Filter /FlateDecode >> stream xڅTn0 WV`Ebh([҃Q^&^ K-v=Ab88y"x~<n!T G\Tp`Jr<; Ld9;RL }ўe_+@ RBDJθ*H^3UHUΫOִ4e UJXHYn'^@AYbKQ{. Rznxtc4NbEg?c2'ج; piݼf%f1$i#)z. ھ P;Ӛf1G_ )EkK`P1]u & ؊nMnDAmdM-DImtvt=:4&YP}]RW]s@SR@!bP(ҷRIӄr. Ŝ(R d MgRP;Z_}r=XqxfןlD:am.$+k{#o +:<}z%2ҊzY$pOѹB)l Ab{9pbFJhn$J]ߦ"`et:46n}`R@߇KLhz##[{ldo 7ARj}^=SyI endstream endobj 51 0 obj << /Type /Page /Contents 52 0 R /Resources 50 0 R /MediaBox [0 0 595.276 841.89] /Parent 49 0 R >> endobj 35 0 obj << /Type /XObject /Subtype /Image /Width 529 /Height 349 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 57 0 R /Length 4723 /Filter /FlateDecode >> stream x[rHFakGH4 z(F ܓ`$g 6]v[Qa ,ڹL_d8֫ժnuf;&n]q/qQUgJHI7 m9N5X!Q0%ƘfS)0Tҷ)>N~QĠ @= e(@P e(@P e(@2 ea^}fǎt=eY 'M\q eeP Te'cvU~\n,S2+wW!0_e)>w?j!ۓ'f)8(@P2((22(@P2A@AP2 ʠ e(@Pe[Yף} R|2}WuIbGF)>#I7qJoPǗ6a55ۘ}Cɚ.!N08ZĠ @= e(@A@ e(@ʠ ʠ e(@PeeP2 ea^}fǎt=e'M\q Lh4hc~_21d]7qJ/b" Ef)(e18s'Of à e(@ʠ ʠ e(@PeePPe(@= (22((22((22(@A[Yף} Red섓:_Rema-&~v2(j8*c 2vݴxr<{78 QVV>N|cjePސ:ߔcõK MPe(22((22((22(@A@A@A@ ʠ ʠ ʠ ePPeePPeePqnZwnApE;Q[ʠ 2()v_jo.lM 'ӸGxsSz2~Q[eT=B2&d2G3<_ Bʠ ʠ ʠ PeeN*@A(2((22((22(2PePʠ ʠ ʠ ʠ A2(2PePG߭V0=8ePE;Pa|;2(qI]>D2&H2(~n7-, 7Os"&qJ{cUUˢJYex2(@ 7.v]bPePz(2(ePe ʠ \2(2T 0R@APePe ʠ A2(Z2ʠ ( 2ʠ (e)e)e)20?DePHPeD(2(e)e)20?SeA"Q@!ea^]z0?S2ֻU2P2(xTa_jo.lM _㛧qd(қ~<V"Ye}Gg( (?yB!e:2ϔPe)\ʠ 3eP(2(C$ePePeD ʠ 2(LeP2(Ag A!eA"Qe2(C$3e@;Q2(AgeP2D3BQeDfB 2̄(2ϔAAg ʠ 3ePePe)C$P(ʠ (C;pEz{ezw E:\;%8S E"i]?Xo:2D"}2 %NQ~\n?ܽ1u汫 FR(JU>I$2v? gj J$DR(P"i'K%vI*'"i'eDR(cGD$QQ(J$r %T")W(H I;$eHƎ2H") }"H e(È(H %B?;nv(γwL|٫Nϳ4Up[\$Gv0R_u.kݠy*Sj/wSM0UJuKp0㵭^_tN.1xZ|B/8jihsfK ~ 0<+__7DfY*-g.'xskeJgsET |`k\̪Z}*~M%ď׷zu]:w{mtN#}˶n_2g& P?/l|xHkYw g͞g\EkTQ߭tI#L]ӑgH)I&b"'nIP^[M*DuiZQF\O˼(ǫ_HR%ͅ=OK½.hm u=[q5{f)#}xC1Y]TF-I굅Jtit˒noKHßK.UjM>M*{`ay&,)#{fe=FR5+;%Y2,>Te,cpfLr˗%Bkֵz2]ZDIWctiiA8}YVy5xmֆ̸`ߥ<Ho WiJ$R5lty3ˏ>kc*PkۚjRF&Q,U2nɶPR5ް 4%_J`YVE+̙:/2W PulvF׼~*#q[.ѲяgK=чs#u}qx0}jվQ2*ZP.MØHu{eR0rd,E*c1>|8 Gde(g`^jhYIQ᏷zm ]:C ۫#4(&n_4gFWD2vI(HRPn_DN")ۗJ$$TODN";'"PƎ2H") #P")H J$DR(P"i'K%vIW`$IDR(T(IܾBDR(/HI$(CD2vODIeDR(cGFDDR(+HRPn_DN")ۗJ$$TODN";'"PƎ2H") #P")H J$$B}DN"IED$$yjnGGJ]egUWgiwL|٫Nϳ4Rp[\4Wc7H 4O4Rm|AoQqI*TJ5Lp0U)ե_cG-M-8UφNwXHU<ߗsd['D珿̟gaޓhU:]|B͟?'\{BտJXQUa@+>R84j]Eze[TMH$Gmm4O}~'.=OCҽhu PF*#]H-TM/Ʌzd" 5a]fk#n:8Vu^5vE=:/է(c,|Gƫt+T-JuG8D*ҹְz',.hr,=e#u%^6 Ra:m]KhMW{ryW84:C]3k}>g8R׷ >H ӗ&X{~/cJOØt{ؕZ4Ū`RʈSc7*B_%y})H? ߼WFt[0RPU>X1?*4pqD#UA%. Q{&Ee? r]:@ n۫ƮТD1Y׷+߯CDR(J(HRPn_DN")ۗJ$$TODN";'"PƎ2H") #P")H J$DR(P"i'K%vIW`$IDR(L endstream endobj 57 0 obj << /Type /XObject /Subtype /Image /Width 529 /Height 349 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 382 /Filter /FlateDecode >> stream x! .4Kͅ endstream endobj 53 0 obj << /D [51 0 R /XYZ 102.884 738.009 null] >> endobj 14 0 obj << /D [51 0 R /XYZ 102.884 713.103 null] >> endobj 54 0 obj << /D [51 0 R /XYZ 205.925 320.083 null] >> endobj 18 0 obj << /D [51 0 R /XYZ 102.884 227.583 null] >> endobj 22 0 obj << /D [51 0 R /XYZ 102.884 196.32 null] >> endobj 50 0 obj << /Font << /F37 43 0 R /F59 46 0 R /F16 45 0 R /F58 44 0 R /F15 55 0 R /F40 56 0 R >> /XObject << /Im1 35 0 R >> /ProcSet [ /PDF /Text /ImageC ] >> endobj 60 0 obj << /Length 1517 /Filter /FlateDecode >> stream xڵXK6W21|jKhP 9hmf+@=w,ڌݶ[Ù7 gpv!f3B1e Dn][.nf$f47mkn߾|CrH J/RHQ1I}I)iEiK}:~Q]JE'V"#a,3_|" kdT I)de٭>l f*.#Ij]o?V}pc;ge#.HDLF2XzC ̲#3 iVPHj/rU$E‘(a!'$pJ NeVQ&Bjԅ3]`)#`Fa JHqDq(NFã3ǚm(( Ữs3hGT }= Vsښ>AjSYgElBچ8Π?|T}ozE "1"yRB?c=yty/d5ЙK^;]8!enKn =t3e6-H Du[@qu :%z73A6H}g.@վZrD?vAe#X}+Wf](2alv}Mvd "/XL J*M g zC^"n> 5Jk֢?>lu57vA I.?u U;PMxln;δSb_LǪ L80k.H^W'ca0ΔѿW30ukb]|M _6؄pcݾ'mCcF ~a<4usAQ;Y_" YH2Inl{$+T9jh ]T B!: QjA0V|v*) $lpb`Ó$ 8UGN 0 َ󇞉Hy/J|ak[sZIPx]xmd|kb]fcWW3,e,䄔m5qB R#3>ەq#iN-g2v*8-Uɟt8|56b85 갪T;+;&"Nt`a )B/ŜkUtm  s}I SwSH@mh>z誺 zy>"~q6@%X ^G]G7c{i OnNOiw%'/P&&`Fr'-3B7O`pj樜Φ4X3,)(&e uv}|NNH6M!&Ǧ;/ ˥ĝAd%hN̢H Z\z w.Bt4?p#K XyMbur'7(?O!߾Z ֈ3h d?$ endstream endobj 59 0 obj << /Type /Page /Contents 60 0 R /Resources 58 0 R /MediaBox [0 0 595.276 841.89] /Parent 49 0 R >> endobj 61 0 obj << /D [59 0 R /XYZ 102.884 738.009 null] >> endobj 26 0 obj << /D [59 0 R /XYZ 102.884 424.36 null] >> endobj 58 0 obj << /Font << /F58 44 0 R /F15 55 0 R /F40 56 0 R /F41 62 0 R /F46 63 0 R /F38 64 0 R /F16 45 0 R /F43 65 0 R >> /ProcSet [ /PDF /Text ] >> endobj 68 0 obj << /Length 1560 /Filter /FlateDecode >> stream xڽXM6W21&"h mѢ{(AT%o$;m}EJ=JpޛG]B64\<lɕۄqJ0i]:N(V1;nK?o3\bz&ˉz(p qOی]/uWCt7y_E.zV:-Né n0WmEB.GEsvEY; qiqJEWmJF(Erc[{S2斖X`"M%c %96kwvo76 c1u"!Z$2)74%C${0m'.5n|-ß A, 2gb2D*x3;9Vz_ݾLc kY4Ϝ1de`S;#`m̛B&ox% 3V$nuF ]U yvV9OtYwBɯ˜ Qwx䑀Sj>Pwj^x{=J3fR9D,i?"4#KuŽ0PX]`m1D EzsԴqNlf`{fXE,g;u,çi* +DD1TSƁ T%$,ҳ&I8 OЏ&~&Іa|@McfpjxSq3~5TkS )xLfLLY_"+i|&`Ŀ&$C5!QEs fuDpbD/T9BLTȄ,6m fVwTE :gՄy#.bBJcAje M-':/]z^+@9+'_?Vt0;wH GfJE^! 6XrP2AT|ȦB1WX$52"QYpXhyܭKB\Pe3Ь[5uugu\UZI)yV,?/WxoB _Α endstream endobj 67 0 obj << /Type /Page /Contents 68 0 R /Resources 66 0 R /MediaBox [0 0 595.276 841.89] /Parent 49 0 R >> endobj 69 0 obj << /D [67 0 R /XYZ 102.884 738.009 null] >> endobj 30 0 obj << /D [67 0 R /XYZ 102.884 469.871 null] >> endobj 66 0 obj << /Font << /F58 44 0 R /F16 45 0 R /F40 56 0 R /F15 55 0 R /F43 65 0 R /F38 64 0 R /F41 62 0 R /F46 63 0 R >> /ProcSet [ /PDF /Text ] >> endobj 70 0 obj [777.8 277.8 777.8 500 777.8 500 777.8 777.8 777.8 777.8 777.8 777.8 777.8 1000 500 500 777.8 777.8 777.8 777.8 777.8 777.8 777.8 777.8 777.8 777.8 777.8 777.8 1000 1000 777.8 777.8 1000 1000 500 500 1000 1000 1000 777.8 1000 1000 611.1 611.1 1000 1000 1000 777.8 275 1000 666.7 666.7 888.9 888.9 0 0 555.6 555.6 666.7 500 722.2 722.2 777.8 777.8 611.1 798.5 656.8 526.5 771.4 527.8 718.7 594.9 844.5 544.5 677.8 761.9 689.7 1200.9 820.5 796.1 695.6 816.7 847.5 605.6 544.6 625.8 612.8 987.8 713.3 668.3 724.7 666.7 666.7 666.7 666.7 666.7 611.1 611.1 444.4 444.4 444.4 444.4 500 500 388.9 388.9 277.8 500 500 611.1 500 277.8 833.3] endobj 71 0 obj [531.3 531.3 531.3 531.3 531.3 531.3 531.3 531.3 531.3 295.1 295.1 295.1 826.4] endobj 72 0 obj [1444.4] endobj 73 0 obj [564.4 454.5 460.2 546.7 492.9 510.4 505.6 612.3 361.7 429.7 553.2 317.1 939.8 644.7 513.5 534.8 474.4 479.5 491.3 383.7 615.2 517.4 762.5 598.1] endobj 74 0 obj [507.9 433.7 395.4 427.7 483.1 456.3 346.1 563.7 571.2 589.1 483.8 427.7 555.4 505 556.5 425.2 527.8 579.5 613.4 636.6 609.7 458.2 577.1 808.9 505 354.2 641.4 979.2 979.2 979.2 979.2 272 272 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 272 272 761.6 489.6 761.6 489.6 516.9 734 743.9 700.5 813 724.8 633.8 772.4 811.3 431.9 541.2 833 666.2 947.3 784.1 748.3 631.1 775.5 745.3 602.2 573.9 665 570.8 924.4 812.6 568.1 670.2 380.8 380.8 380.8 979.2 979.2 410.9 514 416.3 421.4 508.8 453.8 482.6 468.9 563.7 334 405.1 509.3 291.7 856.5 584.5 470.7 491.4 434.1 441.3 461.2 353.6 557.3 473.4 699.9 556.4] endobj 75 0 obj [489.6 734 435.2 489.6 707.2 761.6 489.6 883.8 992.6 761.6 272 272 489.6 816 489.6 816 761.6 272 380.8 380.8 489.6 761.6 272 326.4 272 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 489.6 272 272 272 761.6 462.4 462.4 761.6 734 693.4 707.2 747.8 666.2 639 768.3 734 353.2 503 761.2 611.8 897.2 734 761.6 666.2 761.6 720.6 544 707.2 734 734 1006 734 734 598.4 272] endobj 77 0 obj [499.9 449.9 299.9 449.9 499.9 299.9 299.9 449.9 249.9 799.8 549.9 499.9] endobj 78 0 obj [306.6 533.4 533.4 533.4 533.4 533.4 533.4 533.4 533.4 533.4 533.4 533.4 306.6 306.6 816.9 816.9 816.9 505.1 816.9 787.1 745.3 760.2 802 717 688.6 823.6 787.1 390.2 546.9 815.4 660.3 957.2 787.1 816.9 717 816.9 773.7 590.1 760.2 787.1 787.1 1070.6 787.1 787.1 646.8 306.6 533.4 306.6 646.8 816.9 306.6 620 586.3 597.8 631.4 563.7 541.2 648.5 620 304.1] endobj 79 0 obj [489.5 978.9 0 380.7 271.9 299.1 571 543.8 543.8 815.8 815.8 489.5 271.9 489.5 815.8 489.5 815.8 761.4 271.9 380.7 380.7 489.5 761.4 271.9 326.3 271.9 489.5 489.5 489.5 489.5 489.5 489.5 489.5 489.5 489.5 489.5 489.5 271.9 271.9 761.4 761.4 761.4 462.3 761.4 733.8 693.2 707 747.6 666 638.8 768.1 733.8 353.2 502.9 761 611.7 897 733.8 761.4 666 761.4 720.4 543.8 707 733.8 733.8 1005.8 733.8 733.8 598.2 271.9 489.5 271.9 598.2 761.4 271.9 489.5 543.8 435.1 543.8 435.1 299.1 489.5 543.8 271.9 299.1 516.7 271.9 815.8 543.8 489.5 543.8 516.7 380.7 386.1 380.7 543.8 516.7 707 516.7 516.7 435.1 489.5 271.9 489.5 598.2 163.2 733.8 733.8 707 707 747.6 666 666 768.1 611.7 611.7 611.7 733.8 733.8 774.8 761.4 720.4 720.4 543.8 543.8 543.8 707 707 733.8 733.8 733.8 598.2 598.2 598.2 842.6 353.2 543.8 435.1 489.5 489.5 435.1 435.1 654.6 435.1 435.1 489.5 271.9 387.3 329 543.8 543.8 543.8 489.5 380.7 380.7 386.1 386.1 386.1 380.7 380.7 543.8 543.8 516.7 435.1 435.1 435.1 571 271.9 462.3 625.4 733.8 733.8 733.8 733.8 733.8 733.8 883.6 707 666 666 666 666 353.2 353.2 353.2 353.2 747.6 733.8 761.4 761.4 761.4 761.4 761.4 992.3 761.4 733.8 733.8 733.8 733.8 733.8 611.7 1087.7 489.5 489.5 489.5 489.5 489.5 489.5 707 435.1 435.1 435.1 435.1 435.1 271.9 271.9 271.9 326.3 489.5 543.8 489.5 489.5 489.5 489.5 489.5 761.4] endobj 80 0 obj [656.1 624.8 624.8 937.3 937.3 562.4 342.5 562.4 937.3 562.4 937.3 874.8 312.4 437.4 437.4 562.4 874.8 312.4 374.9 312.4 562.4 562.4 562.4 562.4 562.4 562.4 562.4 562.4 562.4 562.4 562.4 312.4 312.4 874.8 874.8 874.8 531.1 874.8 849.3 799.6 812.3 862.1 738.2 707 884 879.4 418.9 580.9 880.6 675.8 1066.9 879.4 844.7 768.3 844.7 838.9 624.8 782.2 864.4 849.3 1161.8 849.3 849.3 687.3 312.4 562.4 312.4 687.3 874.8 312.4 546.7 624.8 499.9 624.8 513.2 343.7 562.4 624.8 312.4 343.7 593.6 312.4 937.3 624.8 562.4 624.8 593.6 459.4 443.6 437.4 624.8 593.6 812.3 593.6 593.6 499.9 562.4 312.4 562.4 687.3 187.5 849.3 849.3 812.3 812.3 862.1 738.2 738.2 884 675.8 675.8 675.8 879.4 879.4 893.3 844.7 838.9 838.9 624.8 624.8 624.8 782.2 782.2 864.4 864.4 849.3 687.3 687.3 687.3 981.2 418.9 624.8 514.9 546.7 546.7 499.9 499.9 783.9 513.2 513.2 562.4 312.4 480.8 378 624.8 624.8 624.8 562.4 459.4 459.4 443.6 443.6 443.6 437.4 437.4 624.8 624.8 593.6 499.9 499.9 499.9 656.1 342.5 531.1 718.6 849.3 849.3 849.3 849.3 849.3 849.3 1018.3 812.3 738.2 738.2 738.2 738.2 418.9 418.9 418.9 418.9 862.1 879.4 844.7 844.7 844.7 844.7 844.7 1143.2 874.8 864.4 864.4 864.4 864.4 849.3 705.8 1249.7 546.7 546.7 546.7 546.7 546.7 546.7 812.3 499.9 513.2 513.2] endobj 81 0 obj [305.5 549.9 549.9 549.9 549.9 549.9 549.9 549.9 549.9 549.9 549.9 549.9 305.5 305.5 855.4 855.4 855.4 519.3 855.4 830.2 781.7 794.3 842.8 721.6 691 864.3 859.8 404.9 567.8 860.8 660.5 1043 859.8 825.8 751.1 825.8 817.3 611 764.7 845 830.2 1135.7 830.2 830.2 672.1 305.5 549.9 305.5 672.1 855.4 305.5 549.9 611 488.8 611 500 336 549.9 611 305.5 336 580.4 305.5 916.4 611 549.9 611 580.4 446.3 433.8 427.7 611 580.4 794.3 580.4 580.4 488.8 549.9 305.5 549.9 672.1 183.3 830.2 830.2 794.3 794.3 842.8 721.6 721.6 864.3 660.5 660.5 660.5 859.8 859.8 873.3 825.8 817.3 817.3 611 611 611 764.7 764.7 845 845 830.2 672.1 672.1 672.1 954.8 404.9 611 501.7 549.9 549.9 488.8 488.8 761.2 500 500 549.9 305.5 463.6 369.6 611 611 611 549.9 446.3 446.3 433.8 433.8 433.8 427.7 427.7 611 611 580.4 488.8 488.8 488.8 641.5 335 519.3 702.6 830.2 830.2 830.2 830.2 830.2 830.2 995.5 794.3 721.6 721.6 721.6 721.6 404.9 404.9 404.9 404.9 842.8 859.8 825.8 825.8 825.8 825.8 825.8 1117.7 855.4 845 845 845 845 830.2 690 1221.9 549.9 549.9 549.9 549.9 549.9 549.9 794.3 488.8 500 500] endobj 82 0 obj [539.5 539.5 539.5 539.5 539.5 539.5 539.5 539.5 539.5 299.7 299.7 839.2 839.2 839.2 509.5 839.2 814.3 766.8 779.2 826.7 707.7 677.7 847.9 843.4 394.7 557 844.2 647.8 1023.2 843.4 810.1 736.8 810.1 799.3 599.4 750.1 828.8 814.3 1114 814.3 814.3 659.3 299.7 539.5 299.7 659.3 839.2 299.7 539.5 599.4 479.5 599.4 489.1 329.7 539.5 599.4 299.7 329.7 569.4 299.7 899.1 599.4 539.5 599.4 569.4 435.5 425.6 419.6 599.4 569.4 779.2 569.4 569.4 479.5 539.5 299.7 539.5 659.3 179.8 814.3 814.3 779.2 779.2 826.7 707.7 707.7 847.9 647.8 647.8 647.8 843.4 843.4 856.7 810.1 799.3 799.3 599.4 599.4 599.4 750.1 750.1 828.8 828.8 814.3 659.3 659.3 659.3 934.1 394.7 599.4 490.6 539.5 539.5 479.5 479.5 742.3 489.1 489.1 539.5 299.7 449.3 362.6 599.4 599.4 599.4 539.5 435.5 435.5 425.6 425.6 425.6 419.6 419.6 599.4 599.4 569.4 479.5 479.5 479.5 629.4 328.8 509.5 689.3 814.3 814.3 814.3 814.3 814.3 814.3 976.6 779.2 707.7 707.7] endobj 83 0 obj [475.9 475.9 475.9 475.9 475.9 475.9 475.9 475.9 475.9 475.9 264.2 264.2 740.6 740.6 740.6 449.5 740.6 714 674.3 687.7 727.3 647.9 621.4 747.2 714 343.4 489.1 740.4 594.9 872.8 714 740.6 647.9 740.6 700.8 528.9 687.7 714 714 978.6 714 714 581.8 264.2 475.9 264.2 581.8 740.6 264.2 475.9 528.9 423 528.9 423 290.7 475.9 528.9 264.2 290.7 502.4 264.2 793.5 528.9 475.9 528.9 502.4 370.1 375.4 370.1 528.9 502.4 687.7 502.4 502.4 423 475.9 264.2 475.9 581.8 158.3 714 714 687.7 687.7 727.3 647.9 647.9 747.2 594.9 594.9 594.9 714 714 753.8 740.6 700.8 700.8 528.9 528.9 528.9 687.7 687.7 714 714 714 581.8 581.8 581.8 819.8 343.4 528.9 423 475.9 475.9 423 423 631.1 423 423 475.9 264.2 370.3 319.8 528.9 528.9 528.9 475.9 370.1 370.1 375.4 375.4 375.4 370.1 370.1 528.9 528.9 502.4 423 423 423 555.3 264.2 449.5 608.3 714 714 714 714 714 714 859.6 687.7 647.9 647.9 647.9 647.9 343.4 343.4 343.4 343.4 727.3 714 740.6 740.6 740.6 740.6 740.6 965.5 740.6 714 714 714 714 714 594.9 1058.2 475.9 475.9 475.9 475.9 475.9 475.9 687.7 423 423 423] endobj 84 0 obj [462.6 462.6 462.6 462.6 462.6 462.6 462.6 462.6 462.6 462.6 256.8 256.8 719.8 719.8 719.8 436.9 719.8 693.6 655.3 668.3 706.7 629.5 603.8 726.1 693.6 333.5 475.2 719.3 578.1 847.9 693.6 719.8 629.5 719.8 681 514 668.3 693.6 693.6 950.8 693.6 693.6 565.5 256.8 462.6 256.8 565.5 719.8 256.8 462.6 514 411.1 514 411.1 282.5 462.6 514 256.8 282.5 488.3 256.8 771.2 514 462.6 514 488.3 359.7 364.8 359.7 514 488.3 668.3 488.3] endobj 85 0 obj << /Length1 757 /Length2 1108 /Length3 0 /Length 1645 /Filter /FlateDecode >> stream xڭiTWǥE"1bYªP)R[CfBF K ZDPR*UTRQTVq iT~/s{eX-`4;;9[sqC}8hnnt(tG@ W52 /{Ք0EqQ A$C`\fB!:!$FA$)wi(QKNQ#B`>ɞ`ٜ(>!B_ L$Na  XΔFB`Iͬ\!cB8K!RCa>W(0 ̈́ FFk0.eNc?1I4BH|60Ap%D"r4p L4QWW`/M_HBlJAT[:ݜ 'pqo((>bg|S`闛oMSt_EWW|CO:ª1㵥&%1nJ d؞P H4PGYxO41I'Z%e\è>}xrVkl҉ﲜ]bX 9;S-# \F&٨hjf}XSG _O|Rqd~ɣ6̌>VЖuڬF7fX4TsP{=?h#.˞gO;|Zz:~?z1əh}.:/G2'7v;ydt?Q?[[u2 ݥˎx̉-孮]g/M#{,u6c=2]qŎ,P| EvUujeZ[XFQv$bSVsj 2֜,%Ue&8 Rb/Felr[WrlL2Fu*Կ"7'G[]>rCU۞TMO=h)_J>XߝlL׃ ~-ߺr}Sa}sy~M0Clł=j; 631-?-n!FQ]ԑ|{r{F(%:sGqng4Ntpsr|7%8xUR 2g(_oj|~gu#ƵVY{,>^KkB[]C 6t1IGU<-2湛Z4 5825GPWǓ oε.Z+z*\wDǭ>xlmф(*^g?('A6^ũTᑷdtx1s۹W#5a+lMCF WFf3 rܕ姱ŵo )'oOιV&7xZ1"_ׯ]Z>zH]6g~ScwtMc.nB*1omh+63aר&_<0-h.{9+j٨109dLlTi~uqxg*bqtO& endstream endobj 86 0 obj << /Type /FontDescriptor /FontName /YQJSDJ+CMEX10 /Flags 4 /FontBBox [-24 -2960 1454 772] /Ascent 40 /CapHeight 0 /Descent -600 /ItalicAngle 0 /StemV 47 /XHeight 431 /CharSet (/summationdisplay) /FontFile 85 0 R >> endobj 87 0 obj << /Length1 922 /Length2 3736 /Length3 0 /Length 4360 /Filter /FlateDecode >> stream xڭVeXڥt]] 083Н҈ )(%&RBJ @AB@Cs]gzֽu??^~nc3QUA{B rD8X7pY2rRrRR:psՅ~U:gow8B0P_ PE"ӟ7p)abC@M I*|#1G^l<;lӐ9;C4ކ^5*Q57 =K$Ttߌ|f@A4גg 6bɉӝۡaQg-W>kcJ0ӥY^:f{5?z{&GN-[Mnٹ쮠]Sc«l_oO+\ǧeSqI(9740?~ǟҍO''O3jwS`~Ŭ@٠dX7>Dʂj@U%"ۈ %t*?Jz5CK5̞j}1j1i%mZa{D_s+!a|һRKޗC Ưl=ΩWt!'.wZ›TӎpY.]f /ʎnfQ?hd_)=Gy)hWy^NKն;^;InfءtS<X Zg[Rd٣5,j p K .ѻtj2o3ϙ!Y)ҚD1EA.5I-=uŏ5I?;:Q=D{wm``Eey8I;W{c}> PƟH^Œ'>i-=]^6nc=/h풵lڒWB=`\N>nܮN'tTXbD-|,,8qeuJ2k,ˌwCޓ6wEG*b{Օ;[#4*܋o/(/ >e qM|NG缣 AVo+n᝴HZ0l2QLkrlTYv[GW-; . B5'CRfʶ9+2=G!څA$+\M4Hjat}z"[mkۮ=#jj^=N`hO|ZqkR/v;7-Gݕayr"Jh.yS8)wےJL٦8q#&Ik ':i7%oH0p<ٿJ9 [h,!M$ռ'>xY-`y&ZǺ8ܿb.[}?+7T)ۑn|4 s 7{O|4hαolOVHzqܫb#?OT}8,P .ͼu3+-1zxB0? k")xp5{}e ƮAj{Ҕ}D 3FQإŲjX٨4O.ҰAXr)qJBBʚT3]_7AR:+mZ'ʠw"u}li&=QgNqh瓖z/@Pq6:fe|o7'sj+Fb|7 C]Y y_&iu{-dPZx= wV% ~в!p~@jAp|F+ci*iv݁UXDB٧בsYcS}ܗe:qs=[+iB~f2%>XmɣYVv׀B# $Ɇ6R gM=J1' .gNa IOauWs>P*b Zt7wBy8͞E#-xew?Cvs A9uR=#7_ =*H ͍z {t(@KLJ7o (^3eN_jwKMR=ݥWm=U{!q$L<2sӈܐ|˄E Z Vߝk8E8tlZUt7AޤGq~㬖S70yqU/myk&@& ;gZt5 ,_v|;ˇƷA?ӕ5׍@,u)v }9KӰLvvVk:{;Q|+gJNE  h7+ND1~i1,j}h:Zdbq9jQƄbj)+Cx -oN-./,^|?XTө&KykD4jw'YQ|_d@BEm+9* sy>Ο9L1nTR;N>k#*Ko -u[_f?>3F(S u^nl$ȭr0iGYn[Z7?%X 847cf çc廧qr=1> endobj 89 0 obj << /Length1 835 /Length2 2662 /Length3 0 /Length 3236 /Filter /FlateDecode >> stream xڭRyD9gQEcIb ($o Q +'~gX"Mv!q2L9ˀ- 8WKA2$~[h[X(_%@ ?_fUD%`0%d}.>G.T*6ƲB @ ?CoULL*k1P/!(_ź P?A"X/P#T$+O'5$,?8*${X ?㡏RpQnu1š%^2 Zk35G/lAhA7B龴&P3rZN;;ZOXMe#{OnW);n~dΣ^qyLjmʷt 'jt |ϥ&TԵgR)7ׄvb! LfE.V.?OdtY[[B!'5-q?~r:;͟1"?nWz)[? 'IN@,SRyD ~Yq˶{=ɩi_PزxEb6/rK AW 0u(LyfaWB|̒K~!C\T`6N];flg[-Vs0"敧2 %&-jQK\gܦx ;'!}&ϑb'"&;M6G L,52HQ'}65 :5lA\KY>ռEh6 tvbw9 q@ +6+wsG)׭[ޣ:xϺ-bThl}F\J;;:TTRUML$ y|?*_l>D4OIT'qOjpۆdk+ o-O0)=N]9{AQaSA ;=vZ|}ŧNyew4TڦǞ(?vC^$!f4]Cxأ6~T Smsz%c@>=ehjbšADIDoխ}ۉ^d:_-_\ߖxCʹHMw'SbSٖ֙nnEC+C=! D_\ z6v{Ijx@$ǣ skz#b3Bb3ڼ+%t2Iy$A~t4G&J6v3C^ؑ4 = ſ^-u c_3wx^THop6uf_`%дBZn{rjTWmд i p9YQ6 bs;cȭK/S {_FZ'pGeɉ7dz?j6熺G:Pt6<_بٞ[H]AEVF9 Bo6bWLnL?1]H}-LbEOOrqq سFGz:38kĚ^"ʖJJ~K)D^iitnTem|mh_VַKÁ}TQ78Ǽu7-o=@f4M/ y'us r|u{l;|7v^sh\/wM 膳TtZipܝ*;22R ^2ǂz5_N;̾D:t*xQc6~Mlw~`BOpҭwP#ܒLOf="RVW?_=tfdR::^~+zЅ?Mz7nE WϠ|k ޯgzl!5{_Ý?p=:( 8pu)iLti䁡4:[O:Crn5Sh q]"g]:9Pˍ_05mNTa}27DK!a=I\ nMnz|@Ȇ|?9"/[olvDCW 0C5v۰VW9VۧI82 ;^ÄcE\W?1}'$aipITL|9~dsh+84 5H5 an.:n E\5]ykڌΰ?7o괵1z}LAPYIcYz)z+IcUX$8f۲03fL_  +=\N\9E endstream endobj 90 0 obj << /Type /FontDescriptor /FontName /JEOAOO+CMMI8 /Flags 4 /FontBBox [-24 -250 1110 750] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle -14 /StemV 78 /XHeight 431 /CharSet (/a/i/k/m/n/p/x) /FontFile 89 0 R >> endobj 91 0 obj << /Length1 1001 /Length2 3032 /Length3 0 /Length 3694 /Filter /FlateDecode >> stream xڭeXTk .I)7"(504("H42 a CDɑ2P )!{/_k-)nf!s pX< tMa ") L]EWPPftqx8+#HxLCD   A_BX`0$.; !TaW.?K]qXt @Mq^ U?hSGC7B'&@<`s?Cm_L@Wא@:Xw4(B(_ t5C OԹ5hfd,k?}f`6:<+@`@^X$ ,ˠZ@ @a]A,@M3 px^@z4FU!R7QP7P(RSw*Ţ$)Pܿmu>Q}A C(Q u~mj9vj[?j43gp y H*+W%O톢@$@/N, /xA$Ӛ]X'ӢȌVnf>+Knf}yV`-)8(ݫʴ:Gߕ,3;gqXޯ%+BE*z\ܜ+ç,Z)jğ+5HP(z١fVg;Og8b?ybظsJG w+ lz Bw_k.>lj6em@VGu /2c4ݚ6ta!lLp# # 3/2kەs[H},s1/yK]ef7KԌ/B1~XeO=*Bg+LC37Mî,ˆGd$z=%lkդb[!m}36u$+mz6Z۝3lB}{z/o\nr=-8j^ ;#vCZ .*q1%#O,J7E=JH qi)yo$iw=d͎vB)6FD~i—"C,Pa!R]d:|i$rx27( YCQ_Jݞ'Ѝ٭-t4 $w^s+0̺KBo9mJȳ^oOP3Q}nC5.E{iSG(6 cBU3Bi3Zl[1* g-|>R #Ugz21I O4AsX<44)E[w]E=vJO clp|F1E@9;Ev6s!Li숢FS1Uf[ޜI|:Vo󡬫 un܊5adi!`3z.֘]Ԋ0f|۝_=c4&Ǭ^WAZU$Yk4m?6:o;]fXc ҝo^uCף oθ|N Xl, t+1JA1YKi_^N~yJ'_ `0)wnE;*nj$MߚAa))JP!5ci }r.UCimM<"s'4Li"#NvH`I=hUe oq^e b߰/5*jxo*$+_艑xfN[:ioObŃ]ΨPeT.ng%2oX645y9I.~du]Am9K207ǩ]Ƭfl'g˽7l姤p#ȝ1++AI>o>Bq?v7BL\v*xXS}aZ$3Z"bXmFs=46b18*;kO2:d%zYk6 f. e(?z{8ydi篻8(#ܱFjo):yȒ NYol)Y/&)Mv}\$D:$,.tM,2gbr7Hes:dtI^-"D4?N;(ڎOmK削9ŷ%] K{Lv%zdgߣk9|ozxLfӦ6P7춞:xJiN8T)Xs#ʈT 9[KKRHekܘז^(⍋t|Hj.{;m9WGp=ad=2a.\a4 l{O}  ׈xີ~1k`K UFp+[G{Am Ϭ. O5\0fQf4ܷ{[ 5 ~tM0GZ< +0嶈&c("*QO$Zn6&+{Um[j= ϑI{sVe|OLmMU ωxFuMӭh-azmt.:N hkg>S7bb*W>C\:d;oYQYgx?{S,,XCG?{Chf>H7+qˇ0 ar{d v֯ e%@ֶ<{)YWua{UF(Oƣ>Z7́)( endstream endobj 92 0 obj << /Type /FontDescriptor /FontName /ISQPKL+CMR12 /Flags 4 /FontBBox [-34 -251 988 750] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle 0 /StemV 65 /XHeight 431 /CharSet (/bracketleft/equal/five/four/macron/nine/one/parenleft/parenright/semicolon/seven/six/three/two/zero) /FontFile 91 0 R >> endobj 93 0 obj << /Length1 804 /Length2 1621 /Length3 0 /Length 2177 /Filter /FlateDecode >> stream xڭRy<F2 ٲT^DƌmHf%QR2˼̢13pőJYJR%RKɒlmDlGܗn~<<߯:YL.$sB#lXL[ 08Fhcfa0EƆh`'AgEl 0 2~:@N JG@C@6 @8/1+~,rP<v&u"ŤTiʂa'S?x8Dbs9 paQA6g'ݚ HYG0 A!! CUdR6ǶjOr"[*E@L?hWk_5 ^hOO1XT @a)aH.8 & kb8p G ȕeaC8^ "F?0,&n!h egOFƢ):~\6drVo G Cf L/?s;N^!9NOFTd !o&[<_<@JA뫛Ս)k+ڀp NSϲhIuD)o#o0*F2ًS>Y3u|x z>7=}i4s$U$mϾhfq1]33e#r#r;ug~9sLD+^E@wnsbNގ?~xO-@&fk7tFi.KM Tk7G.|3N'wN#d^{!WE>kALH߫n~ =]:!lHż G S\j\x,! endstream endobj 94 0 obj << /Type /FontDescriptor /FontName /BRJPEZ+CMR8 /Flags 4 /FontBBox [-36 -250 1070 750] /Ascent 694 /CapHeight 683 /Descent -194 /ItalicAngle 0 /StemV 76 /XHeight 431 /CharSet (/equal/four/one/three/two) /FontFile 93 0 R >> endobj 95 0 obj << /Length1 769 /Length2 718 /Length3 0 /Length 1247 /Filter /FlateDecode >> stream xڭRkPWŎȰԈ @$CE _MXMvfI!QCtDa*P0#@SSiAv~}g(d hChG)Q@"ϗ3cIZ0(4H, H>:CSX-@ cz(iId Ѓ8L$Y $m"( 0a} LoΤ,41a4͂a&SǴQAku2 & C AcӐRk CDQԇFHĐ,TF8VF5Urߏ{1IL:D\s)1$" _aN$bI3! * HGIs,P4\4fĶVjIʠvE@`}A6˻t?q fhJ%q@?.ORqI㋲6y*3yթ~]M0+%Gcgْ'v7tL_/:0t+ꧩgrk *yzјAuL9,׫C%ITutn=:x7I_`wϞeq07sfVFv_ 3R1K~=rVK胮jC.ָmY9tׇN3J3ߖa[.w_]7yfJj]:kA.e:}uB8P`M>!%D3b/! ,ldBAyMMwZIw{*s6=9oهAV{Js*۫#ͥ^, q+oݴ4KLCeYNkԎ{M=~I Ykɻoja۩ _ᣙ*Mʊu[}v&84k;[QzszuOEoCt۞glP:x߱^a%Aofݨ&L2ʳg}N.D6<ݟ9{moeΎ+YK@Ay Z5HlGѻUso˜> endobj 97 0 obj << /Length1 727 /Length2 17568 /Length3 0 /Length 18129 /Filter /FlateDecode >> stream xlspo-v~m֎mgǶm۶X;m>{[W_zvY=z^f-2"q{;gUS&:&zFn3##LNٔnjP1u010D<--Tv~XZ:~ػZ[x]]]]\]٤bj p0YژD5%j S;SGC1@ɔ `f`logb/NN&`j13G{[*@LAU`hgg?hSc#UG p,' `bi 025anRvfM\?hH015bc#ohk upq6uٛ:mLbΦv&&EZx/B=Q?.K'qKwSEK$73GMs>Js&v67?;bv&v5t4ǿÊRoOZH&a_Kݽ蘙Yt,L&v.V3khjo1ڲ1OUZkhX\%85y[|SbljMiDgfJ.▜I8´[xAiqd4qYmfQg3[4´݅6"d.JWװРۇgj4:ʾ#?xJrF+|͕pjh"dyYPa&x_^hcOK4W@Zb4*}6~ #B UA=Ћm u灒,Y uޥ5wމehaHmJ ofN(|yPed+Vٗ ŎB<^<_>̌)gy~^z?%;;inn*lRZ ]ưraz{L?^^F;\y jN+Os'R"/[GvaeOs]Q+됣",!`yzsC<N5E.h:Ƒ} TN-)gL9GR*ϢiȹV쵄^qk k{ta0m%J7.F!c$;ƳP a8L +I2|Iual9^A)PS p1>[m_m'[Z{֠ұ^o0. r.gfU z_m'=<lb閰 ,fNs83U܋@ BEwc44/zZՉ" o\s $](Sqj&gk9]^4H+9< \6⟏+d@O f iEW$SaN"ymyN܋pka9`'j.^ -WEXrAC 9bP9d.c!$CzZ5qM?"Z!H/o2$ <*JH`EǸe'쇰on˸SŸ9viYࢋ[P J鿏S3 $jDE3ѽ#ce=[Cocj6y%4N=՘4dDI1N@Nl 3ԴitܽJ5T6%ě:1?}sϙE/foU&-:rPlKz! ~[嘫`S-UYדIa':z 7bhٮὑ4#70\!S/ت?YV:"`l-rv&C+'@=uKp;l𕜄HVQ->lsy>e&ug91Ym[Q`+ 3U.>?Q+&kY3ynzGwhl%K''rU*68-P?nk 7ʜ!BVf ̂m StP`06TqR>An7Gr- u]c4_e9=.~fJf!G|O䶶~h|=gB ^CsKTE_*էvvf{!H͖_{*{v1Nd8h̝iAvK %a 6FrSṘl@zy)_-+ʡka%x"tE΂õP*u+౞fE,@'M$9ZI0FNu ,:A`s{H (&wmjFГ9e'O҅ O:>Qsi Zzd&eM=SZTHǧj, ɸˇWQCS'd΍^VM |P"RTl+wiTk3cc36Nz޻{ ZhRC(O:$jM+ F-J{Ҵ1LM˚5]mF Mrhl/|JהKaPḚ`̂;%HD I 77{scW|?^iK K9vƐXo9CuY(C ʐ=?0g|H (S? d 1pM@جT![=Ns[lN'Z:٧:,&~5OpWty]peU@1og?zrXr?g:ֶl6HTE/Deh灤o_Zv|Ij.Lh,\kTRVDJB&^wM=)Jth)sѮuԂH"!ߌ5A榴5șd;KYo{tO b `%3ԛt\RMսLl([ZYU/I\7עȨu) \^΅x\vp;F\vV%(IvJ}DIF̼cQ8bVٓɠlοv{}&N=~8@w&G4;Fͨ\]v(? #<ƃST`T)%і\(Mz!û>c[LV"C!= r}|82?o(#eRB~T7*_e6l+"w*of3xKw\5!A'K}E.Ǫ܃&9p$vF\7[ K|l~Н寖f)&}=j2pkڱ{A7gp:A;oŃuJ*cz‡ ;"]_\[d x99c&7Db4TQ0Le/w,qe%׳ʹXeyw؅h!b71mM-j?|٘hc-"gr1V{wT߁w pb1+5v#xJM~ sA{6/sEX{@u^GaF]rG"!/Pl/c<0a '3NHU Yl,"Q֘,[E* =҇)ۗޒ&fr2BF2?4 )hx:b E(Ί2Ɗi`qZ{յ̤D 1j;K<'kPm3Ѳ#k% ֝|ȌFLuJj,Z&=$t^be6!ELiD PR,=h?׆ՌILV'x `3c\bC)WD~`uhXP˨]pn+ʙ2v aF(+쵬?k H®( (94Ɍ(RT]B3N73xiM\*uosYO*՗5.ڢ%yP@$8>#]P3ktȥ:cGms\v%T%O:ljէj vRH8`x|MVs|%R(rɃ$ñ>.64|R6c_dTQR>f7arKh`RdfqEJBsIT +D7J$k>O:g?ՠ,cH ɣR'VsmhuS$j>2F#=u {v:2P{FRǻZxٔ=u]l*ǐ$($ |Y;,L!T3#傾&Xܨ;fQ[bj}Ezb9y;g [>V}W5(to"EX3#A%/6&LTJͧ(} NEK]~]EHkg\J1?䯌kgSfJ OJ/`a\̺v/"6#iBlzЏĭ=gMF߶X]~8Q[Htӎ0Yۗ9L&G瘿PbVFDx{@`sljlÄ<3٫Os. u1?J/{ *1ec@4,i ȍ/fVŦSBPVH&xhi3?ZebW3U祃NrRr£MN< 4H&A๲V#8=:{D{5B#TT m83PII=@n-OL7AVG߹hl6*v} O`Ip7}dBižݷI0,3zQ8,xޚs w\=N]e[6 aoMWręL5G "o#V5M%9V*[!ǂ] .h#$B,mY'o.Ň?IOM{cwYj֡U|"]RL93FB h8LPؓ( o2kH4nM=-s$PmGlzKfWYEWs('%cOw"{XP! pQo7=* [g?<]y\}E^1 7*=juy>“cK1{HK7v؉3EQ 01SzR/@4=nOx'#=UW?;kZYt7u0.  kf(.&ش)MP^Q>;׌77Oko :Az {|=U# ӹ|EIss,ĐkYKy]Pq7 .ufXThdWPv,-w"L~M,ۣI`Z}LŴ WC_AD%!voh̪۳OmeU& wqzbRFHf';Bw\aJc(~:® oԃ䫠)A>:=zi*x8E:t 0 sYCr#z͝dBYPʚmIEpg{%ʖ#,r f DsP]}F'5i"y5cN31URz3X=/L/o=SJ֓B.w hGzS3噥{xr3)Iđ$ڋO*0*7ɹ诺z=W}?HW/il8N7KЗ/Yq7z0k)$\@TtoKG4n$+ֳ-e@^jYOcîߞ06,~?kUB2Nw`lu;oꖪ|2Mj\l^~L%Kጭ_L{R0\sV6v%7~!ԩˬG|lp=y#5|mL^X",v@Oن½ldHs&gIL= !1 D(R5)6:j*VQ3 <×_$إ7֊ɴ=4ǫVM| [c ,c1!X-̟&z泆WbΦ0PQ#d*$ qY4.w{gFKʕ+XnF[@͆9Dk$.u{JIŖ)EJP4t@r^f`!.[ԇ?xv氃碓uUJFYzqeWXwdaN!I6 2i }>*똑FZycc߸L f=`ۣUi^֨vlS[N #?/%l.1,:9h`Z/llae7Tjq^>T 5{mYȺt7xR/ת饭v>R6ˊ4b ٖq]R1Bc9K9pQAc@l:s+L*+Ը~itZ v0Z+4_ ,0wʍ~|)cE&0K!/PD 8f'b 2anBhԯ NF^鮘²kq!q| k90@ Jdj?Q|e-;O }՗uGR*ʼn (c*A\D>.&Y_UdşDd#9AV (=PmJroC{mCtjtUq/-y+Lh :1G4'7"CDw=AZ5u#&Ӡ6)Nmq?ۿ;GP9bWB ݺX=<|O;XljcC- xQU2*SzfQv.龧͌4qG  hM;Lk( {a Kp 1Hnj]g:XD:Ύ-,eu9 FL3<8m>U|Q揇Ʃdup~o|5άys^N*`]"OGpm!`G( f)`On5Ih8sGt D׏d'`*씰@mU!^KDkprV8"cKQzz^TQ5 9y= n>XkCk+e0slky٨pMargm+$;X; GiZK +хI8~'[[ 3Fɋ%c}gy(# =DzzLMΥ?ills0SObP`dXTk&~7|WdlvNbƾDazBiկeiU!%Ap Z;z(H rh'tΉb /qJ %9QR{ 礰@xQ-(fJ6~!ThץM^XAj~Ր 1lCiWyxӫ|z/mTte@oxiib_? Z8:O-I3@.XJ~Zz+n=*oN6%a${|Ѝ}!tVr-71:(5pG]F6 -(|.;U?.V)ϩ'vr8e oIx%ᐷC˧ 3^q_X{\!n2 ǺPAY)`'rڴ-@qpz>)HqtV*M8aX[m ɩNVAJc-!p6 <1^tǢK@ɗ'7F66p|K\Թq =+a@cEϸv3&tkI&1TOunvvFᰰ,u.L3 &X\IkZEp(^)`/GMcX~^G.8N#Z[WKpHwWGXWbtñ!}#.j_J{DEh>?Niq<\F$Uy|j: ^BBmcxN0~/rh q#w40&R4Yn#O() `⤵Ϭ6YVh2xlnPo מۜjdx\]|/Ah&Y9T uaڡ vv9'c]g:"Ȕ@Rf udok;>Z('+Qgn ~t}^[m_J5;;V1 TkoW7^oc&|L;UmcDY[?NCXg6_Y "Vvaoh^ % {ls!5ˉ!-p1X2;l}|2Y,4l4Ob./]a!@Z&: F>w,{gIEEs!^;;έ{A$5&Cs$Ksnt{aq o\ ~q{]74* < heDtq4xJyǾ vy;}R|kvp.rb˽U6q~z +Z*Z̀eۦfLt2=L~p} ֚< }yu~;&Mm̎B)G<{v8y0raҽVɁ*ٜv$7<6Na{z>%E$* B䩗(sszEv❱ >KϪֺVz=stVG"Li@ ( )ݪ&z"3cE8-jj%sW48/tRqZ<RAŬMQÒcPB w &2mlq˹_۶e?P|~"m/" 6s[W{Vj[T}غiaH 4\4(xoΌw)Ү-!2`esk0+50V Xs|Y Tϸ r˧ҢWx]^}מw9r*"Hy#Gŷ<ԯ-NB^mlg&:5htYfwLڊ]\H̩K66*jVܒG%VxKQz`U$@d/pf3<Xhmz!L)|9+.RM'yZ fLvc qɂ4N A=t%~#paѢBmn|e20zxmF\Ȭ>!Gu/*\EdHؿHd4ܜi!4AA;Q+ŔWPkH,LZn+emG|霬%@m 04w 'mG֭-Ϯ) 1 njz{1Ʈ]+t2^ Y#BɿbVu#O|²b5k!؍yUaTyMW. o$ow}0 Jև%LY&MQJ&{־z%ʈ(N%k& Mc9'b论-kkƬe7;fW(iLaZHނuh _ሊzn 64)Et2W+ejZ1sxL;tż*,]sOS P!Ox㢁ii6جІ!5nezPɼ,OMӈ @Fz>喽$ox|3J3T%E>Z-X*L$";?yxTULn۹xN??—e%O ul*ײ1uY!)I0#yQ}0d ʊD<P$d+s27vρ 9TpS]nAv=& KѴw#P/1Y+qY9~G &D)EH:53;ܻT 5]lf,EnvUB&.%f[`drHj˨%=zYU zq_mZ6Fu&JT )9\Oq緛3D/F|VcsP*PS%g >;~F Ev%mxB9&/ J.DᾫD8V ֟İXbx:=Gu^H*vQJKpԆA>\3Ņ{hc$<#6?p9w`@#Rm]x4pM5? pޚ`{D)l(:Rj,PwO` *,{y+vrRh&lȫ%t€6x t9@_1B;5;VQJK1c cbc]0%^%%I5=࡜0]Sqz-Sga4ͫ?&F>rXV3ŕI^& . ѩM[B2öCe(9+2Qlt9[zCZT|㣡K_O1)l7VP)Jo[HxTE&{V&y(?޾KSj!5bNzB\2n%U0ZƜњޝ5ND2995x ݺC[2X|&I~Tk^Ĝq:lvaԆ)(!(BUyz֏0< xvZs{2Kκd9)$ ]3r(3}wx0 B /^qL,n&;Ʃ iyʐx4bP;O. R 7>i<{[~Aq}vƚk$`Q T [zFptZJ{X3Yd)Y{!ŷ(7"Qd^ΝX$'$g\i+hd8\L9z6-GGOgô 1u+6 +"<w1goKvT NsRAi:~x@5&z%/+=$w,?fXH[ЧꃲB:`$nVp4ezTIqy׿)ʭ.GM w(?3Uґ}cІ>?hwD25c&[U? ѩ +QfG7,~/Y\mG'sKE;`^Q.僭juAF4+;c@ܼ=>L%tR5 ,0A"3y;Juk/#kSBjCgʈCYXЙR{X`gØhO LDGM+`iw7%O`¤;>'4 eزHC=+T38ib_)4r^X6,<gMmg~w|gx*|R #HAj;F)8XW(+p!,[`-_O̷P^$91L#{2Ǝc rK-»}{j8ƋHT(սQOZS@<,ӜXfA A j:SD,gox6"_G4Z:5C53`*W5L_ E[K6p-z(lwVvM8+r.eX $"a+ȸyė'q*<gGWPlsOK6_tɤ</LR [=Ds\2Uo{Mt=0z&dw%.ndQ7q=_bK+ di.y`~hV ChowjiC?_8vSsҝ6"[v trniRҗPC=!ka !h>muoIxM B7 jO%1+=qIToB4>քgUCkhW K9{ Wz,lr}/ذO&20W HXpRl,.}L^6q?&YUWO༸~&Txa(+f;f`{[>Z*] |ǀ1GߪVЮU<7+WDL'4?\ံZ3r,<| DIZ+uלZɻx8, ]nݧu6YrטfOAv$bYf}FϐǑ V]oYZKkn"b PqUuC3/&7d<s {YTI@ endstream endobj 98 0 obj << /Type /FontDescriptor /FontName /XAQESQ+SFBX1200 /Flags 4 /FontBBox [-223 -316 1694 925] /Ascent 690 /CapHeight 690 /Descent -194 /ItalicAngle 0 /StemV 50 /XHeight 444 /CharSet (/C/E/F/L/M/P/a/agrave/c/d/e/eacute/egrave/f/ff/ffi/hyphen/i/k/l/m/n/o/one/p/period/q/quoteright/r/s/t/three/two/u/v/y) /FontFile 97 0 R >> endobj 99 0 obj << /Length1 727 /Length2 12693 /Length3 0 /Length 13286 /Filter /FlateDecode >> stream xmcp&\-v?mN:N:m۶mɓtmvl{gܺU_?k^{]uK۹t0egf`Jh21XEL ], ]Ly&US3  jdin2wnchbikPw4𹹹 928 0Sjj p0YژD$T jIS;S'C1@ٔ`f03'gs3ur-@^O(OQ @Nr;gЦF?e8h;ws8{ pLMMQ>*r 3t#q1Xkf-ƙXO<кc{=[}Ru>e[6Iص(h~7dO1%qQB q'l&|*O+_5Ǜ귳U]m&ᘢ Ώi+,C%hXŗ5.:zn27zؾq%b/w#k3p6Ul㸰)&ES5>-2;Yr:إ @B8|t`)\_yFڦ]:u4>].랤I5 #5LPD}ҩ}ŴʹȢq>)#ߓE݂K՞v@sIT:X-wdֻۇPI޶DRWBzZ6yJWnBCC ,Ӻf ZEhĝqb{Q"6ǒ>Y?yR{͹s1堸h_zrvRߦ"Ff%҃Ivߤ!YgSa~H$7>p̃f Q_5頁6s)iU.S a^veͽߙ3tU(e Y+9J.k-}vPxnIJdK~EiLH(>~d ۋ 0E +E ov4use7zgBQv)Gqls#$N{W*}Di1%t|**Op$+}rL[l1$2o>s{p:20xemNy\b"xs!>LܜDgNrf#Mc)GhV *d\ljyZ+[f#A }Cߺ=qF<‘ '=k x^z3zM*?|O`ݺl,u}J k^+n\X2-/QR01fA@;|{qY2)=[ ^YiJ"fZ09upY>w@"+9Xć+D$93rvZ0ܝ5PnS$0<=^N<07 Ц_#ҿx)}r(}RUj }$v`Q^1'yND%W^<阞Cib>p $I(Gi8([]ʿ>$щ V]j V-]zu.pS a41@Rrv*2H_$9˝*[g!XUT~Jc4يV{D$}cft\.QĘm_$lsVW}9nwY@<ސ\(V=$fIcWBj 1_PJO ):ygGYB:XVhpl4xP1t%+b\g1!B3ǓeSa9ɲyH-67@߮ y6ްtmя!Ee)? j209aAd P -u-PKDZQ4E=u EI[da(W07dJZ^.{#?|dʰ] DzOI!'4⇕,Ilύ~gS.D>uծVp0ccDֶU|pBE-K>QTvq7$3^%ߑLFryvp:$F_(RD^͓ԑm7_D~ 9L rr+,VDMq]*weit: -_,~zmw;^I( \5i/6YGa vM:l[fet`g\ sZ_`.hi oIGԤ"|jD i^)9B#5Ⲱ>W !D*Xx"le''7KD7(F~GE2Lͪ aR!;6GPQ-(I̗LZ_3K @"!3yy<_/$6FB7Qwm3/W.)"1(tB×PeKtk+)0p꨿5I(ԁ~1 gKw,pOk4 3LzT4c5;1 mN!8W ŤK1t+_8>K95t.7tIi6Y瞉Ӊ(Df~ge yPa x^w5|[eE2!\%W;M'f5= k= 2fhNVǭSY`qX4 IEi[;@O](SL\O3xO,zN:(K]G~}(O:YTEЀI}܀8'FD\SYkюc3\XFt"Ο,n)It/.'<vWUmLs)Щ d"D"6 |ɱ5PEPC7 ׳k/J!_ֵ PyExD'?xTv򧝢5:2G>D r]UJ߰2~Р7Ib[rWΐ9juJYQ_wly;G#4S@7p;c؉3\4fyeN}Sxv.ַHz^ =iA<Jt~/`{()  hu[G;ԫe\[V.ZW];?UgU[@͌>C߬N-6S%qDA!dL59g[1 Aw$j*ҤbdZE9k@˧:k,Q[:S,㜳&$#i3yvF p-Eo\\C]'H-QL%&9S糜wg3DHF(Z98% 8lxW  ~ tIa\aZ*aHy+F_TNwLya7O8?fom̵eQ[N%-|HS$P-f6HA%( Z{{y)tsR"o/ZvHWPNO ϧ(&{K|0=Wa7>l%b=06TO ݡb I Аub2:OF{,]:C18Wlgm 3,_'9Q(^» <є }kx(7n |&knɌۿ`wDꟹl10W iEc%FUlbp[g6_I"d,gÊ[rSGǗO RYϲ7@mc# i0) oLH/<յ~#-a˾lVIZFƗϑѯH%4P*#79VQ]K}m2ʦV>T&? N[CǤgAf֔?W\UTeSgTMS= y6x6`Չxqa$+Gs}ȥ> dp8!YmmZ9>Y6AbEQtZCWI%ve:{FN>;m~g&4}[ƒ홉TGetʠ>RԵA hkx#3_^q;#D)$F 4{eH4J 36UµԜ*{KP딛ѴTz$5$4y0`J ]31B y#k*7%UK1LhtS <j%chI?GY vVpQp-Ϊ%tϕX1TގX/V*cfb$M̶j5pB%a\ZEAb:o;nE0;g}1=~|л@w,/%\GT{?&)7_ `"xbP4[Rʄ:?˺E-X?Vӛz J'A_nt7VMd+0.݃r9QYM.o&K|loZmlr s"|_Z X&qcv Ȥ JƘ'cʡO]ͭ f/lIڥ$n|2cŃ'؎Mt?T:3YP{o/ [x/Du{UDMgD2y t+2 UUm݇C ;܎72PԷRţ(-diUk(QeMc[p KGy+b G{Mvv*=0B'XJ곯~#aeucVoVԶ S>,K75V v`0Is5\4>)ﵺ#/Lj1E+NC^u@b9O4>E#ԥ55O#"}` D;hS0J71bք 7[:j7 B~F[Be"GTNHk!NEgB`C'9o>8 ^1 q&H_ .kW$7,',ȮhIRxpkw(n΄lzpH#DR .iCˬL$~X;%;#8 R2zv ؅unƊsNs[{8 ֺ`ٺ$@@hZ7F)p͇ TsF A Ǿi-/i kySyT~|^k 8gyE(&'c^JxNdU?N6aj ݺÆmDn5)8,:(AuL:SmirVz[/ 1w'q`_zy "/q\?)zǖ`'C<4{Td ijgt@z40Cћ{WQ/@=)}u!*7V L7=.AF7΋ bp 9$T{xwkDf& /z=WڂF?ml4$G\݊q1}wuـqpJ,uTt=md vїWPb_Ֆ頎(?aR0X0ժ- ͿC^l&XRhԓ+t*(Gm0 zIR*a1 c1P5mٴD}/dџڌK,k[ZvbY6Dג*R`Su?"1;X攇x}hWv(QǞd[QAc<⩰>=h \-0ȢmͮDAluG.Pߪ\g@•ۜ'Ԗ*|dX\-9&fsJuu/62C{ 6M>r5iajDm2?B\oT/b~fmR[>lG #\׹&g1 ~VpIQJ2?Ϭ:hS핪 @#DYco*!U|J齂`M-d -MQ(g&dJE>׀J2Ig|s[TG5hi.Xêwʶ*S¯I"Hjqrun !D2S$/R8HAG)~x6|u ^XY>2St) rqLY ̵`#+\|/f>,K\.ﰈ܌/3u2O)>$A`3ȾʌՅTw>5^!B_ D9SՉu썳j03 C]i X!=mи Nd?ʪ7Yqό~k1z"nyݞ}=a;SB,4{U8YyIOK'@z77(zaNR٬|9xǯjȜ`7nfԋb(s|{ka[LOR~7ueRdy\ *b1)7J3GD9v[<*ly#. z=1^Sy,wT0,.bUQ"q׳&$ёi_y"!ͶiaK u 8b؛OW.JV.i/Ӌ3V ZC5!;y9OPȹ0Rq V%]r3* evqc=4wY"xsqZ'5G[]qj[nM&`p8qdȃ)8uDf)"^ 3̟~# Q:tzvO[//jݑXj5xVh*_BkyY\ S1n4q$2ـS1r߇Wv:N|aZƙ2/q{zgLTTH C5 z/ƒc8 %٠gҽ@Ik/fiM3GGIO*Up:6qcHĘP/H AC?19u)뿶#fɖ=G,3# T➋X:E[ӷ-|$T<֏N' %vȤXݓݑK#oЖ w2R6VU AiN#]bQaD#w J%\mFKKlSԍ+qe,^BzEj:X\&.96 Q/yy=p00=ef.C6Bg4W 'fHEҰt pv9f}2<;MpX=]q{P4W@`!4WiD @ [e_jvxj%.%+E겒cS70yjF4?(֔ fW#"7b!T)>gZ`O8(wYvEnr5J? ]kd [#kBFg{SQFOsP6B~NIdv/rT]d36Ўs7I$SWNмJAcT\ҸYU:/I&effh֖rHwxx"s&,oԯFm} +ѭ6WyM4]W3Oq>}ak]=83W>iѝ϶ݐe"ż#twξ=[K:?" 2N*"hIjM$R̲ ;wԃ +. ~ 1~@KRDs]z]ex0.bJ 4̓UF >هsMoNo>-fm.=A#ς-J3 Mp*5pNF5=WT /%~Ę%]^ٷ4jmdæ'KIu (&F:[X j4a'^dz` {Ǎ g;c]?3)->th"X@&*Yp@!vDQ%: U.OP+N#׸'1[:`cYk%|n2椸]9CcYր剢$ްR yfۿ_jӐvyW;pt\,0 ;~cHdAa-CSVR'oJID~4y0@De c"5!$==`W%Cݪy݄Z𩝬Xv?%IC6&i8$V_5e.WػV+{9bqY%LAE(p>l[b\qa,Kc]$fv6)TZg5~1޽5?8aٰ%࿆.%Ue8bHAGB9Rو pZ"b ~sE kG!A ̻ʡl:v05D &jԤZۜya#ҁ}2VIbK+#ZLCCH)U(%l,*Q.> wW]n}Qb fTt u`b ?I+L^o>}x$\kB(oh}s5aڣs2 2e7cx\m޽jz#Am6RΠ鍚We3d$KLaPrϟRj⎧>.@:9~ endstream endobj 100 0 obj << /Type /FontDescriptor /FontName /ZCDABV+SFBX1440 /Flags 4 /FontBBox [-218 -316 1652 915] /Ascent 690 /CapHeight 690 /Descent -194 /ItalicAngle 0 /StemV 50 /XHeight 444 /CharSet (/G/N/R/T/a/b/e/eacute/h/i/l/m/n/o/one/p/period/q/r/s/t/three/two/u) /FontFile 99 0 R >> endobj 101 0 obj << /Length1 727 /Length2 11564 /Length3 0 /Length 12144 /Filter /FlateDecode >> stream xmxspvvvlmIޱmfc6NFNc6yygZuk&vwr34uy9l,`SW+{ISW0?@ h66vDj+HOmk rh;:[-.n,n,4`%`ne H)d2`{-@ P]sgd/N.,w;%f`PVQHIjJLAEٿ.`翦cײ5e_Yg[,`GdnrAnI%/V psts;@`g{- AAYzs5_rT\JnnjW:Ͽo+s9zOÿӎUSECVYkOZhU?iUSg+:[y X$lc/J\Ӈ`a|f?tn`{d; W-+?1@[&*7u!,'Aږ5xgESlm(7FQ'ޗeβ PA_sRLH^lZo R=,u |l=xU,kH: ÍƝS(ל JEJZe07`!Ȳ?uw3 A9G}4wMHsJb4F$RƋڌcc'`yY򺫦KjBOc^CgXM(E3TvYT#/l>jR1Ht'⫄FfbBsRr=Í;Xq ۿVo~u3ljn_qGPG=JC_\GeaXU~l~JGĐg+M^=]ܔ俚:FuLPtƭDo7j}-| F$= ǡ@S32UZ$AŤPRt-BFC1RjGަvU&XvOF(}m0ć\$$!۲KTwÆX3C߶F{x~C 5Jv$~@p\wBIW/ىCk}U=wwn~%g{|*tP (7J؋xKp4}]aBQCɚD;a@z[>>2iXIM11/NJg.`f+%Pr%c I"34(ȌZI@&I zJlfLf3kjJᶆ[5Bl\'ŵ0) ` }bEo4K7Ʀz[ֶӗkm ޹pc`h 64RplZMo5-۳oµ;Kgůp7" !DiCRnGe#OG2~o;S|ynmQ.ħ&G2:7y hTw%p[Zf0*K/Rx03ľG mV4>1BsҾHktJ+C]$ޭI7ׇ)jȑ|nnpV@nlgLKu1lu U DM&%=MT; {)Zʕr>$M;`DYo0W>*=d=w2t^521Ki@N^_wzFՆ[;9ԍNnY&*3)ʩy{ԕO݈o\C_) NR@NldTb-"}-B-ћ%6$<*v_ޓiUnS=5IVy L(RSxo ?ȣ=X4x5+)|h9ԑ#= #QpWn {X^<CxzW Rǧo]ɰleX* 92_g%P ɿWinF0t瘹|Q\xD7 p$b=3*\gKߥR<$}RWSM,$/B_J_;U1G"-uJkd(0,叵&a Qf)Ĭ>5Խɷ;/*;դ#XăV[N%q66j-y8]H?SW:_(MfsI:M2iVbǡCӁ׃ *wks kuX4'fF|q!SG$S(!mYJ20/dV)""kBWwzfL#Uoo PR<M =`RR咦ܣMEpd4"c&w2ADC<3]coĕb*eAlž& udrWg TUl"ׄnӕ74P Spj~;}0Y17#䂜_X^B *[#dQTcOa#*GH ,m9z+~{E@S\sFNbhb>[n>3ͳ50WFOgX>p0Ј0Blԝ<>2lJs4GwyukсC^F<^Q-~5y$P6$:YܮA;P7-B*|xvl+ӍB=GޏʥAU^Zi-}5,n w'HgZ( Y4P[y|khQ]`j~I ^$QaNX|@zZiHy3(,B$ N6 #Me lڮ"/O6)r2k}%mberRN6Ÿ`)u{,Zq#+19" ߻/#("c+֜q /USpz]ӎqiݒpkZ+nl⩐O0 7W?AC,*lw(ڬCP$1M0fvR&U4T 8M]<`х"&Щ߄mTd5qwp $O>̐u.J N/:EǠ Qb7/8wPu>NI WbJaiq-+2OK[nk=#.Ms ֔K멼10%`~f ][ r4 EYM#L{R+"*uMtik`}yM09'=+ K0 C"3M*XSl<Cvdԛr7[K(֕j)$˰w^bx4+}'sWVSԔߟvF*`3/8"i8tǸdeK~%h`5`zȕ'|~WGթN|K%ۖ]k2yž6,K7H)fw#sgP6= 1&GLf?10NxA+` 6J b$z L$wDGvN2xRt%>9a a@CfQpf",ë#\l;'wl;\f%$];(Խ~: ~~7w^rAXz]zr.|*Mj0v6hȫvp J"FDNkoMxӏ~Jw>:H:'.~Zr#E<7:ٱ]K1񯺱֑߷K>^&kg!pZ[e֔{_͢Ek8qHLQmY~ڇ>(؜E4$/phf8ND~P;}=[rwyz =9C)DnpH``>Ko|S{#YڿxII%Qbb+o՘hDe0Lg&hS˕)'mGde.wЋltF>4WojsF a5|?dIz NhPcQZ~LqV_" lg!tZ]pu -aVyR{<= zmZ;P͆:Z[OdRyEXadH|j\;"˖?U߄&. Dž&l .M^G|ҔwԉDq`yu7 ZK y1o, FV;P'sQnc8d^v% @xx}qOtyUswP,ƥo1؅o[ϟG]ge`쪷o ` 4y}Na /Ӗ cmK`W};?۴n.4WljuCJhSIj|Bb4Zܴq"ѷJ2^eUuIqJKC5p oeMu_n() zqp0$GT 8=޼)u;=l;Y\\Сaj'^RvNsOl[g$OwfOj3i{l!\bs|nS!mB:05Ӄ?ksnv"E7?i 3h?j7;%,XBҮL<#HkEg"+qN^T#e"3NW/ۑhpkWFrrP7ِ *kn%HhXEav,Wm%J-ʒh0Cd$/~FJtck4H )EQf: l/^M`4A'r4vfѸeM 7)Y,{Q+CrNGB5s 7`>"xǍV~7M/Q6\gilW7tלtH7ZRV)rpR :z%VfHxCU`m\)amPs(k7U|"}c-9@4xE1 p36 (/y$h&Dg={~Odtc XwIm笷c1ZA,N+za;p"ne=J%qg4ro$×-c9GKNi!37{9{?XPm+J~A3eL4?v}Vԏ; {e#ȤJraN/q]f o@)V#{I4)U>׉ KԿT}8] <2ԁI4(~f/.ߜt=/=K Kė0 %qUe6y(.f*|O(?!w4oC-rWuQ#,>+ rӫ(xl_14bb/3>>=5GS7뺊-2]$tbFt;voڝ!|ڃ:h3VhutTEW֞'jr-&ROcTCy9f Ufؑ"Nz#3-Xٌܚ,)=:!銋of2^^wF19*-Hߧb##8gk7Va. W. #Ѻӊ##O=BSs^h?"6A2{F !6,!)n-IFoiO܌s[cniȧ1kIPh-wkN+A~ U9$XJJF+i=1t-UCe! "D0"U-g/iJy^SyjWqk.]u=!z]`ԡtX&bkO|KzUzR{H6RjJ&-]H(%X|Izf]s!6fpſ/4̑<>0?L@5G:i*O^2n^&{ ʹýSoC7<@D\ZZ{CEqG[qXD4)yڇ 4{-. '1`0,Íت +ەkA1 ,W.4jAٜ̋Y@JǶ_ï֚*pA - ]z =HJ7/gR '0k#%ʇT`RMwO_ιѮeG#G02zO/DwᏡOe2xBTΕpvl+T#"^SJ~,RsomaM㉹J,o-j^D-ŧB>@ڐ"JTIK˭|7Z'Y*ܞ޸Wl I 种a*Kh (H]&yV2jmEزv'iViT&z4 KE4_igA?,$țXf/rǿFݎfQɫO ps'9396ZC˞E^ ®}1y%[#0CyzR wAHq4ck5k5 ,\p2'?Ypp9hbUw^%-ӛ)nՎ͜GUl}Wv;kfmdq^< a({;E#AzJr?ص9i6 ~fxUc8=s:aOТ9ݯ;9 US-l?&GsXI^ Z|PBK5ى@I4aP#eKC̽OE~˜G7;3wܤ㶼]s%N!Ut 1nc_+e{{cwxL \FG &V8lU&9} $Տ*?kR?[;'%*oMxѭq!U5\]Iɩ `FF 2< YR uӗO dlp~Je|& H vjFp}X jkfNź3ʚdjIKݟV*LKKe6ވ]W|C)1QD尓hdaBbZ:T=H:y Uءl"K `#96N;Lӛ.m hi^S`0 2`mL^#C[<)*jafa5e CZ"ŀe& @"e<s1̬HF~Xlj|KpA/Yo#v mJ! <OM.<8(z)6M<\ R;gnК`j7b'-TݚᄏE1O̦y]|JXGSWIݩt1S.81\ȍ$:]~AmX""Wո%Aa2x]zStHGtYreb;~iO&Xm,)eeFXcy+ǡiT-U g5Nv2P=BbpX&S];1~t҆H&m_5MذB91=5ǤcyI=RiXey2E|ō=0<1{b.]%X`f,`<4r~д՜.\`eBpyf1yj77Tڍ8KQ&~44,?Cu_}SQ|HUn2WD-z,pkE-7y>s>i%ap .<2_ˎc-/2wUOgH,6y,)Dd2)mNovRas6z%@m! ,1[`U8-rR;" 6lj i=IfsIr#,VO݈ $"W$lT"]13|G!3ahQ>B^nGE~7J*P? \ܜ=ScVlxuTNNc{&{JXA< jgݱtM۔DpO_^dP"^QH(KNx faH ;2D ,N&N?og1{T!&饉ɭ@(/X5&LKEߧ(x5o1C͵hkRŒ8Փ YƵz5־Kaԫ9L<4գwͷ"+MT`<2,:RoNI>:lF\F7f(@rbtE4Jד4Tv`Z1{3\"4{աiwz\MV,!?m%d#ʿuIh9X|D7pr]?6 `|wy}mʴݘ)]nqTtC:/,]t 5Vt:IX+x>1 +hL71<=Ċ3["$ԭvv'dzh`gc5+@ˏ2.4.ߞUkT݇VyKf Ԡ'S %qkFn*|s%cW ~&-(Q"l,2ˑ]@P$B^vlW ߯(>M(;lbl6rCtu4-)8 GBN2߫rg&K@[J[ޓd:J#k.ߊ씝7BMoD30{>YGC0_P\J@\ZFj*֢ڱR^YLZB>o8 {t*ʜE IRnS/-W &{zelxɭ5TXwl1v#Wؔ#ǶJ'S>Z:/Akq}*Oj385(vSlG 8_\ aͺ(/nq.B9ڥ k+rG٪i._b;QDkfA5 &ʻ@E* ~JLԳ., zU*-k1YgTLҗD>v?}}FoZ]їJLm]V endstream endobj 102 0 obj << /Type /FontDescriptor /FontName /TOSHNU+SFBX1728 /Flags 4 /FontBBox [-213 -316 1617 918] /Ascent 690 /CapHeight 690 /Descent -194 /ItalicAngle 0 /StemV 50 /XHeight 444 /CharSet (/Eacute/a/b/c/d/e/i/l/n/o/one/r/t/u/v) /FontFile 101 0 R >> endobj 103 0 obj << /Length1 722 /Length2 5558 /Length3 0 /Length 6125 /Filter /FlateDecode >> stream xmVeTZ]  00 3 JwKI#) -Qwη]w?oy7a 5TUEE\\H0=s0` Fw%b\U8 qq:̠@p/+@K )|$|d <\g P50}o x@( qB0 G;'8  J7q w詛(kUELT@y@UNk9kEm @'##--3 w'uu {vB`*F 0P"P?=@wO e{ ( dwAOwSnA}3nE U: \wHпiC oE=_cJE/II$EV'O$ "gNCRJc4vNj&ZTo{RᗑNM0)mlFsۛi;K"`f6z jlҙTQ.mEv ,eO*wy[d&\xI `(9)zWbױ7Yګ 7j.`'Twlx9Q\u>9t4!ZE&nQzZHl 8ֹA!ǟ[fu`$v}! g]͗]+σ銇  *KE0>Уov0pढӠu4.E#Y1x S~d$r-þ|.\(QL>%IRȋn_m(cI0pR M1?FG,ڝs keZi)WR_s~~w3.ARs 0G _7&Wz7PGv5ᨴ?LV~!mK=dzeS°sx9ti"KGPAJǿ½w;!ߧ@D>Ly1wŪӅIx3۟x(W', 169A1эl2SRH J⋆)~,i=JFTZk;: g3Woav쥶&u_‰|bӁ" /1Pkt)\md? S(e8sTR!T*>x%O5I\}Dp#5D.F1Nn>FH/ H xŅI\ r,䄧]א&gmxۇbp.mtJy44݄^ǜu/6G$֫6R@zI暱_Y*`yC{؄-@}t5: :*HY=zğ8­++ӻ6:mmTa>ivZU%ʎבG&B[K݂ ]r< CtޮȻ[WUDW!>&@T:BLW%?b6%ȅoAADLh-bIs$󄌾6Xm_T"/l̕d_0 "6O,k&glύ=/32R{j.BѾqNbʝ`w79>[6gi^I,Odv!+n/e#vU|0b .sr{۳XStt&(9ٛL).{՛U]k▧bJT6),>V܍9npџ'\ean Dr -tY~ɽ'7E-VK( )x51O$b^"aQ]`Q)Ca]mLJHE'i]kPV<f&E> g}Gl|]q'S(뾅.|]K [ظ/';JEߚBωVOF0sXWl_Og&&ژ.< 7鑺.R}0~&b),Cq>R9;~$" øK-]ntz]DYpmEOMrS6#c4Tl6l|nhnEhݴbox\UVtΑ=!B-dpgm4_rӐ:VC4F8(} R] 0,&ddjܢdBH>kv+gM1έUYScRփ4uEL /K1mvnS~fi~KU_瞰-=E4,RJ%Ȯ"u(iǿ,{Hz8fgg3{'4nJA;"1on {6J>J~z5?r_\=pvY*O"IQRC{ j2a֌+ya4NO"lҴd`} ̢Gq؞{gH7kK=vV,uu2O@qp⸈Ni[D}yٜLk#ȡ@[%2etԞxQeAlÃ/6ei ?]ob]k<+ä1s]7V] /~(NU~i-^hP^|lW{2:3G= ]哱h{W$c.|&EQfViJw7>oWő}~; ge3A3gUm,b1_ce~ sF9lw<C䦣}\Kb%=,L۱[k71a Cfr϶W`?\ '^RT::Js8R+^֋ c*i^>')xP[S^ڻIsuT-¯?$w\D]SیՕ!g_48T5KP^Ѯ )sPoe{aJa3!^!Ɓ꣧a}v|ԥOR5"sKj:MZ)izvHp7H?s ,0L,3\?ϘփV?Ө);-%|-=K9qwk.bȡ9Y+$X9J^O+lP#7b>Ք5\ZxxG6ϡ>a{zUoQ^}K¸9shOu}XnH:v"B{aW6 XnlEhYְqƉTI[*qi>VaI V`jaAKX73$Ӑ`-[*A4j8mC'.1BCin[n-HwS%: F#|Y)I\DHv~ܤ_v-}*lz vcbcӾʀ9bu5{c^\1Mp/c֔^913 %z"wދ.0DO,f-ם势?[~vtZBq>@vub߾^cgě6kAm *̯lZnq+:-(8A|UTLr<\Y01-7aj NM ~SU[}/TX`R!zgOinY+ĤΞ"ZaOB5@"ΙYs98c,O+U_[UvJ Pa˅WJ &ےKeA؞ヷ(]>Jiz+{AON3)y@7~lӐ\ bK76s$juYଐf/m1eɯ|O, :!/Dn{nIEvPzl%ybɞڄaFtRIxp ־^Bwԏjm=OWЀt> #gvRR,Wei-|Gya,:jޣֻc1 if+jF+Vj<{o6+Y_ֹt޾3Gz"'eI&&Xꚠvy-,5$n3mJiRIt$7Oܲg.&ћYN@0&`-Ķ2X5>)g#֕+;KMm)i4)C*FKU/^_ F9u\Ni/p<|̰( ^MH.mIoE NMwt{=o;(r4} Il#].dڏ.rO: E)^}D&'mrX]H`5S˰#֓Th%⃃*T>r BkÃ1l"> endobj 105 0 obj << /Length1 721 /Length2 19624 /Length3 0 /Length 20207 /Filter /FlateDecode >> stream xllo-\ۧ毶m۶mm۶mm}yɗL5֘3kmRB1{;OSFZF:.,#TNŔ njP6u020<,-\Ɣl M,m-j6n7777gW:'W>MM.3KS8B\N njgdhPp54X9R6qv&LovnN.3sȊ˩DUv&\A;kw4Y6g/`bi025nvfM\?hH 015jc#ghk upu1uڛ:m 2?:ڙBb& .HmfhD/&7G:jbogg^^TZNSRﴨ9@- LwZ?_2&e ],= t=t/JHÛ@`d5vur2sfmޘ;*5Oh Xȼ-^6oMixWfJ.▬I2Œ{XAi͛qD;qYmfQG3k ؀݅6!d.7;jWۇ;Gru.}'>ȉW`r5WFé%"K"5gZ([W@}y}TFN?-=3`u1"桷qZЬ r^,nK?>e j68.|ND<)G C]o Vkx2mw:GC̃+Ӧ^p}d1;H Q.vkX1qyafMMM>sp\bLh3=fIT6?c/e"J)Cf8?V+5vB*K@..%nԆK*býgxlxI:dśJZYl#\AZE5+0pB%+ qohx3@zo_̧cJzkk $]@nKp[_)T1Z}hXͤIit٭~@"iDagȍ#8."5[/H_tgLIv(iUFV>糛\zRyz8uYFױXܖI_;A>PYDtDžο ,Py@ t`H󝩅n~jyEeu*[s ]5IۚXEOr=݀9r$:;"7X`!vV_HӌڽNxI݉·k3,SASx!g7>2KG3 ;߲b?9F`Nό RP.;d;recl;qv[GT~>7 `_Ots+j״/WN'\>?Je'H,g_"U҃wn`Ǘ93kUٽl|S, ǔ.ci?L(?z$8*I~ Dצ.󘾵4tVl)XL& &I$p;s&¾L4'YtoUfP)ƅDכSBk0IOsߤy ?_j5}z:XS u sGc@Ga`dzqSb_85d}Eq{ ǸQi6c>oRB_EF @@tG+.]t&$D\VD|6b1Ք_~*&BB`$uR:2M3Eq;ޅ(KR;pqP00߰stM u{l msU}J9OFPb`7u1M:?()iV@*xd2:rZ{C^L-әAqV]uzZu͔47AeY3YHm̙5;o*e:)_je.(oϨHd`y.0W2muCz*sݾ"AkR $r59,#mJk^hZ;;sgeV82e5J^Ue䤣3 Ց7da^J-t99V//W um&EIC3n߼3ûgcK!+P{sn轢wwtgvYzR}!҈qj(.Bw Ț|k3Mڣ<S<#-:Xe:]J}L@1)aB82^oO14aC6;X-,.FW;`VRAD%d^Ǵv"xLg_ns[v3;o/݉ 7BDfҏdj^Um'mLwNjDe9mޠ8^b+Nj9_U.ʏn0e憍4;%\2U̇$IjvC5P"RrTkbKISA{2`UPv`8sWf%j#Bu7,dR~!?9w<̕ƻr7xmn_R/Ax/ߑ+*TC?) ҮFHyeұ3/RƎ?.d s ԯ*Td\]CI^|M#QQA Y/*UAEFK&I1'ǹFtb@hl1*g;/F(! ĭ{M }"o3kƒ) nnV6р0/Uc7*Wǹ3+GA;Ȋo p7n}~5!l)>,Nd?0ַp\OowGʌ ^0,NFX0^.ϥ`z#a:ζ/9sDH3C D@pO6/1z{j8,Y;ۏ0G֢ʭQ?F3 >Ld/QQf]م2(g&ZnG*}88+"JX\UpZr#NޙGs |$wB7`'3a|o3)K'H$0H@^B_|9̕!s5QGfr4C')@bb*@Aj( \e6)+0' y$=aqĢ>)8:)۷% H܃ a.)ީSI{RYg8gFC)n!ad@=OOo1{vP^ j޲,D_&&ϥڂE{cq94Q (L"ը Ƕ#J>* #NuALXZ0?`1}5)E5=EPɑ-['NO],&QIƐE'̰HFi}׃vB9{M!T}5[uaS3h dEaGLzS|wȫ|pNDBYQ!}x?̋ɧ 111jd}*v1A ow|?)J=2SbpN$dS4XpmQ?WhF7!MU:nbZa-Sxeo.[GV'hңĸdckD*j[@ܗuv\M'f3?dֶP +WDBnx9SiC}dl}a*OQ[nQh4j;ljP*%fkR!0 ^1x_* ԚxgVbGK~TPh@YӘ>+4#X?+!R/W1SKv `x@-sV:{׼QP_z oϺ0E"yA"z7xe'61ÿWrB7b+ WG `;p>*H_cP4W T哈$/LH!=D,K# X_ļ͆X-7<&OSňa.ϒLƒL{W 7o_9:SnNhM THa?Skal?d{7 W;_6wbFu$(!T@̓DHI)ep=|.(N910QVaMVrؕ gꆊCnE:`S&bX P%J7ۍirD`0}#9@9Wهb &-Wri|g -{#}By,vP Hۗ_>;t̯SQ,y57'|ڧј5肕1kzfZ 7 ٪y=g`[~x%didWRkИV׷=eчz"Bn#n J+zmg e^߁Y9 ߞ|f)eWׂ I7p=zL`Sʶ0A) om/(݃[D'&u}ϧ݁RZ7W"JjϚKgfB!Xw~F|y1xH\GH6q%%+zC 6z%LbLA A^ p FU[wMNX7_|1sTY ƫ/ϵCTb.O"T}%DŕuSM3uk WLӽ~2KmW׷(޷qQ<ʑg`n6ˀyi"챃te2cޘ?׀ } /zZd`qJ)FdȞ]kD[FfmN?كzX(5G/`HdSNrhD PBOy EÓG$p@EGV_Ed5WC<(\%7"\N nUHDFz}Gd05_'H\wë.+Eb'~1c9SH:).e!/s[{>ԂTsvdԇgG$LrM2N\_FGR|Ug9RREk pqh+|=y.Fٲ;YXgj@4o`dM՟SEorWŹC\Lb!o-:j9r)&ŭ1yv|eW#Ui!us% ~p 8$}_=L9[`<>("nNilC/3SDDob ƕ\Q҈h^F7 !{nCY?ܤw.$`KI4K[*w6[ߓ%_iQBL]aތG}a@#c+ 6Yd +BQr,.|g9)5:9^syȼ|[C!![I;;¬@jhY(JGZH)緃u?s0:k#GO5p>jbB~eT[peCג-Sh"Dj8 (> R<2pP:Ĺ}©~,v^Px  K}b7z 21`pC@`yE B _P2*sfҖ8+ ,ĈR^S.bU?YQ|~a(ea:wOKMt}i4q5)Fia.wMM#ZǑV#@{~_aB\pARs[Oc8:䆩_wvR}m{fΧp !zv'h>Oy w8`M0I"obLhFnr8w\T$,>V J4`WydwR 4 0d88ڏ Az9"*b:թ B j`J JNxTpj"P)=ث |dHy4?h=u"U0sWIĂҎ7Q:ZLJ--8me]v*/Taw$X~]F v{7&ǓMՆ%hNF\qK(h0^:!7=E+n3`^ɢ)qٜ_;);FTι'ouсxi32wא{(OMT t3n&J 1iV[>Ub#~/Z o5Wpߒ<6ohygGb=zEm]1f>ө!-tl^So]-,tcc$ dCxqAω'γ! (Rۑ6%e^ ע'Zu A =xG +/3MQד}5Eőہ^=6`{pK]ֽZ xQX( #ػ)=kT)X-y75smBH\`JO~RXçZIY?ǟ-#RנxYwFjjJ LZ@t}uf<@Fi1 "5Moԯճx8MqQf-PG%pLvC^Hu/~ŒXǂx gu =DߖplCe$si VF@qtiI&z`C؞9`HE|}~r!V)#(˕ԈW] 0+RQs|5;pw!~5~bTqmy(uƊp^S7+mW Z[1fougha#gFZ%?AAtb^/f裨;>PBjB-vO 1nD˓!<AOd([q L+h)Nhuf'SW@)Zv@⾳Qc:3͉vk\0wyoW`«FվhjR+oPz_^u:1A[]4o+w8x(H0vX_m 5]_6f TRO!^\,gߌ 'vS[[pUAtb}If0JCvȨuT[qǏ_y Z`ɿ?T^ 1瘈f׀ie0gUSHZRĆ|YK R=T(4džw4yPpәvKMmtdIU<5}DQY.G*{)˳mRr[R֐3vϙAl@P0/٬[}F3vM{mTT)Wdd<.\ .hd25g :һ4cپ&ZSЩ6pVZ0QU)w|Qӯj7 y1XjAoʙ(W,'8弣4m0.l3Q൒*ړ9%~ApОFtrȵze3*H~g4] C V#:TITM(3")K#~6='H,p3;"V8б#w8:Cf ?;H KpL}jkƝq$DAA { IF=uz;kGyلJ}SỊvxMPoٗM [=(6y$03AtěTa 5-ܧXMZ;<πo4[._RHI[3a~]kiP^yN |oMzl$,pLMan?z,9(Cuy `#Nģ$)5WDP䌄w'P$-140<iq'ĒѠm}F'"^% C 22eS'X2Ge/{)k5?A ] '(ZTkTbKTqy~! ~h_uz L"sR;:PsK00σr4ܣd4r`t ЕIω) WkeZrR#j`agskhk\"LqPN%SqUO7űjTm58$q@]<=WROf>Bz`'#$J4Dqkd/j{ߋ~Xdmcq ^MގD7Wb-U)e]xzK*$jz"9sEBqp̉ ]}B%xN6zl1('J:m'-v@tqwYe{ˬ*>f`; 8bǰD_¦KmɛH#xl!dy'jQG'(fctT3yW+3lVXh~qF._ ln_[Ӭ},3+apW4gہLj! `@ebe|`  0Sbe%GQ 6&1vxU-LIh"ww*zK`$yBɜ2)[oۇ_"nF&/%}ς.-V!51qkx$gcnH?A)ӍoiаƳAPF4NvɘmS-#[Yŧ.3 ?q (i}qK0rJeW<W6{ IĽ "yPs,jqg&y5}6cKi`Շ"jޞ6WWlt7@a"qz?' aNtZ6zT,>pgjR2Vȣ Tv$us)cj,[DY@L5Q?朊8ϝGfT,/ܪZRvY[Y9&$,FUF.3iqtbn7HGN,$e>kscmGCi!`8#(.Ն5Ύl5Qgjӳ{Qws8>KO,ƬzQH CaPnnآ6n7;3ޏ(Ǭ\rj <Ϟ\N.̒T[Z0Flz  OIӓ%G$;f!$Z]tEIh0J:hccJx(I+GCZO;8\ͰjI7\p7Dc,ɼJtʴ0+%=ߌm)HNСiLIYLzyYɸM%jl/>A$Z>`fx;hch!X \ZZ>joF _HFțq+ցg8#M@<m0_mG:{+_LOۯh~b'>XIַ̾`# cBr#uhNB#tؔ\3L+*9 ɾٍR] ,곿e|_P@Ieek=#C| m_QU@bvZ^ӥȨN%K{gIrH1L%#'8GTxr({d0{_̸h[:V7>}#eaV'];In-HhpEIյz<[Sx"c* ADP0D"9LtM*~EH؏LWNCB-' "ӝ,鞜}m^K(c!@$c:*QܓVqG9](8!4#%W< u%`raJh dti$yEwdnJ_:/q:nPI"]ӹf]X_G D*l.SF0l\"=Y8&`B+?ņhhfGhDK? \RKs^IQ_:y9%N8oYN|yh<`['{c xd5cc:O53s! [Pp![^Lq%n 䦂A _bؔx xu7wYv=7k8S٠;W}=E !$Y@z +>*wYƹHև37؊H\T%V<{ޥߢVM*FdIl6(p$gl:`)c?Acp+ #_Q(5>-5(j)! O#+mT;`qh)kw-#OyVv3pҗ_biyK qr}#Iװ}z֘EK gx|n%sӟa@@u&Η>tcCI"V2ωVk/bBܔBczE:0@A8|"BoZeѯcݖ'|Жe,t@X>UyPt H<۸|;NwZg0R%X1grr !my yRƭC'JBt`夒3(Tm YH;Bm Uw@4mݫ /(3i'ٸ~7+_Lҳe75&Y7y!FnKng.]A?gpɨLPby}G1_W5\#&jSCW$[5Мz6uB*uSnj~b+IFaH0|qRɴ:Wm0K2}Of9nd%$r}F[K{?N(qȳ7a: kc-r| F ##P)E* -Qgdo:WԂ(JV K|ܕ&<g+;ųHr>(ݵC]694Q9~x*4\I )YYW; C~(CBvzی1W`KIQ\mvMؾCbQ]qm5aGKnY;\mcٗN>V蛨k%+f+LNR!)aAR2R֍ؒ)Qj6mঌTt I c ٞ>zcoDV7L; 6ݷgbDN7EGE<ځ)&~^< 䄜SOQʪe5X57:fytÞŮ.uQYU?A9jj4ۂunw̹N FG=ޮ[Lskv EɞP~׃ғğu@$|q+յ~d8mDy\=%F(ɺg[` #6gx&DQ{7sBsr,.юdq;#(eu^#B=]Fǚyd,a:Bf1'@]X9d_}x cdKԟ . NAA%\sѲ){< L##Ix6%IMe UsfcBo/[#.;&y_ӕ yHK3UZn5f1%2?̯ʍhyt Nu| EO;4A Eϩ+6W_]c.#ʨ#{+W"[ ˚N 8h)۹'&k`5E4+ꬼmП: RXT 훺¯Yz$e-I(&u;p@Y#B] 5k,I)QpIF+/:b<1Gi*ص,"wvL$1t 8n"S$s m=W +Ar'do]]rN ۓL9`l$e=Wϙ2 :UqQ)B-gΔE8'1^L<|* nP~lOdɴJGN$>)R%6uJNa\k3 A`?/ƐT"m7tڔtF6*ǻ=ܡ؀s/}Db[k~dѿ cTDa^OW6s|ܚM{$rcw#-#y_3AjXҁe/ z&DHM WaRuvO~*ONG(jCTp7СY!T2'Čqg 1Ѿuecaz;lO*dht_T`Gxt mDdhӆ??NZW4}6oU,!&i|ЏbSy;I1!T { 7!,Bch)ޡVY={a!XkNiTl#_#zO9vYIOs9ېI"[T΁?ҹ;J浳17xW)~ŊLm]P.iM_ZpbUxxؾ a|W T M꥞MڋDu,'NIgRǮ&zR5?=7$$&ؼ LnUY4zk- nVW.:pAhau}u<`$HUfwd LqDD^ % $hΆy&afþT|aH%cm;1o7e[9Kjd>v|?([)flXRUzC\ڶHPr<50KDF0;0>uʱ˫'j1m%['(b4p`aWa&1C-mUm%Qh}Gmn5+l xب" t3,ŽpNE[ q!֓.v zvlZO34'&1-ܱ2+z{ 4 `zaٴHbY2c^9灓I]R`27%%_lN_LQ~s$A(bH+Kr)7k0i!kkkfl> endobj 107 0 obj << /Length1 721 /Length2 11337 /Length3 0 /Length 11928 /Filter /FlateDecode >> stream xmuePݲ5dpw'gpkC\B݃n/=[OյWݵzj E %jpq8x! S H h<..n4z4bce`2g؛Z8@:`{5@Mٕ*IX6 G5}U96@\m6 Gg3 ;-lən _a@EVKQU +ͩ% 0u(mwtqC5忑#FB<qqs,l]f +G4h puO L=dX,]UM@&i P[ X6_0\L h9C6@_-MZ ,l\;{ϑ@N 5M .?eYGs@寗Lm=mpobrqk=',))7;[+?r]!???yіm7|͛*EdjSmGZ/Q+-9ґf {]"a^d}k=e%k}hh^Ud=8nL/JFq-dZ`9v bkwd㳷P7F]prR9=QjŹY_J]L,rUq@o&* 3e >}AY8 #h uqu OB(, eW]{}҄layU.̿'PƓk 7$X~_4-ae3*hӻ[qf?&MhO7?V8_MlǑAYp_3ɷ]g]iԟa%My eiyb6bZ?%CF 7Ieײ7K{k3i2`P}9 }Qg1@)R GaMD'X #RT*B%e#~_EZe 5=mmŰ|3Q~*A:`7$WYES*rml]6c#ʬUS]Αɰ߃y^Vi{-hsrxV%8YUA3nI\U7.,EMz'pG ~.V5lWDz(+)xS$ ɤE cf/(+Eɹ*haқ7>%~]_i6z=0 ϭmKLKuuϕiʄR{gIꝜMyiy].d.g!4#(g5MW2R챙lm$"eYe8ƻ.NaK!1(lsR#3enl!u޿>xcoE/^ ~F6;$fKRICק_PEKe($- \ﹿYM/l納$q'c+ɾHR/WM}X#eF~s]0̔<֥H?=A{}j"H E-Yxq_(ZMQ"MYVivŸfP_a&jT:YT|lME~_9oMEk&!6!+V leTDZG 4T;G~x ilV tGO*( 6\$ <\ K$o&@iSg ^)IsJ&FéϘ.[x xswmfY>UWZld9*¶Q `H{CbZ+Myέ.VL W̅:/v|Y9*^X>rݒKlK "(FTid* ޕ.=Q/ް/r;U 8,ľ y ^vxMinREu(K.s>dP':X/Wд?cV _hf)I)&gyP?Mðv07Ci*Zl@rXII͸[ϜHWqpui\oLNDQgY(?>t~*~3}[3<ǟ=ͽ0Ҙ(Mw']W~ercIM`.7U>æJ94g6WYSQy'I1sFv W&I)bm$_Qwvϳx;o/}Z鸆~4V"~-x'y"} *z jEdX:Kma;꾇0Ñ{^Xl~qmC@;Ys̤'jK-V#칼jy\t3?!rC1{-RuED1oQlGUZ+n^sZ#FvW:U-dCշ+MuNS i`a%ͤDeY$zf}/7h[F_ v55!12)uK^C{ؑdD::uTc >r9wԣgpV)6TOp;BD18zZy2oT8t.饉x ܭ9'#k}HR_"4 75dA;$?\M:lƣN-ǫFf?zqjv@ggahʱk#oCn7b]HeVlIusʫf})L`tICWg jfp^]e^Z +CڌWJC"k.'0 5"z]Gɐ'ө*gq':NMnPzGW>Ur\y$+Ԍ:on{7zctC0|^ZmkyN+d^+,[T6U):SlRq/L߫zZ3- ̚CDHH3C=BJz0ۗ~RxЧ'pml],r s`bAoi`gߝ8c8oalT¤Vw|P>X&BP;J,P6pH{gt-% *}Lsڂt(֢| c1};0Lfh'4oh{Dθ2Ni=-HYlk˗_d&7-(s`2GhOhzSTy]?XַLGt cvKpf8>S:\+H1D. Bx,uute( q0=2ONCg03:c')mگR=bbeP0bשׁQ:>Bo#[Rm$UfjqGUΡқ8BGtOX3s6L_ :pu6oSl׵R* <s@eƷKO8 /dq8))&<~[(wקHW:'1Fg:ԗ#U|\Q?Qu(4<ZZA@FUkC#/62w5rvOÅȈYrӓ^*$[}렝àQϷܞ;ChG|J Yr?O-l"‹?(+ ?Kdu נ>2M)_`5)+{5lVZG`:0ǀ)Kbd~g/|lT[*7 ]r8va4g(Te7xF յw1o d ߨc-(a/A ]t 2]٫(ӣU3͞e!m~B4JJG~)⮤ըo?#4Ur٨t`zzR"V7E^#NoOGlvT"2 ?2~܎%5.v͠aL_\HWr+*%{Гz8'JU,'įy|2pJG˘rL,M ~ JπDQ<8pȣY_QLm5z{[w-^836Z/هOd"b`h:kb:%I`3вض)@WE: ɨa"O?(Ƕ $EDʋuB{sʼOѸ'XKJ{S%*yI $>Sc>Ysuu0GwW7b&_`]w((@&cWފ]Nq`4E#-'Fmb=gO`@D+VIkf7Z6DCk RKK5ap!l3iŵF8si lx,pJχGT|_DنA76w<n A6BFgHE +'$dR)+xxbx'%LpVii'; ZV, GN&/_`~<~AD&w}J7ᜊ${8tITR30d r>.)XkW0&hHGbFlp E^ !N@,Cc#q&Ku豐gĠ^[K^x {~mh;G2 V< ӇfQ8ncW)&Cʲzy"~nQ: p.\!'! %ԓ/6anv&-n^ ]rx0U?מpEv 4IvB3[h`05Wr.iM$n Z3-*fO4nj%~xp!G>fjl.ԝmt9i2(4=2Q]? 8x>\>,ԃfS eحO!r:1!=ҊGֳ8Ϥs.4:0ݿ4ǯ5*KMV5ȶ*Ӟs%p{ !د}QZ72x}ۭq>a0e K*0|F@ g'|};ե#N*)aE)cVUG/DϘ=QzNJ#]&L BrvT."EG yoƐ63¾EsOՏ"xt6|Ruw_4j7.Ftjç1VtԯShiF%G!wBkX։\Q 4&(Z+MnnFu@m}fReME[G?z}Yf8̈́dY~ c|A;>{E7D.e}k-7ZsS5< #kV)V\H;Bl)$*%vB\ʎ3B3lV晓x{PU,ޥt*;1OxŰ(cM~Q% ߈nDZZZ+O5=蛳Tg%ZFAQrwAE}b{-|l–)o}?Qu|&/Ҝ .RXDCQMf&Iii -অlѷ\fĥls֕`|XOQ^M5mwfqنGץ} .(N}U?kB?p 3iFnb]}5|2Sdxf:FI64H[a_;qDDѥ=WCE`g`oWF @CR~@@EaQ>m{“~ D/EG Zy-Y:XE 'tb(0 <>\R2<<لAfG&p׹,<$cI|;#0~{z;Ģ"S%(}.` DkH?wFi:Me 7Z#vu}ӫߵSĂCu%tJT2Μ\<$QNFOhO Y@(-'O5aA|0Q%H[3;FNjM\%>6oԱ`WhZxRmrd]"@ CER{"K#iq"@->Jl`VV5PP۬J= ]M(T\{!V=W=YkhCh]yԦs}ĉƚ7XEC/%~)3t^wZݟVxK d2\2=G삡oF ?;\jn94]ZAk_Yev͒&h7M=SLp*|k|GooMZUI|8N^pZFzGv):ŋߧS?̎U3{sڥ^{M3tkaX1X檘PhYe(}RXwހh*r|$ ά`Ϩp#)rTF!iԏY5+Ϙ,3q?@g14p7{W)PkI~uЕ%L8)WY?6wD]t(_}u>+3%w|>;)^܄ᙴ"`ߌϋt;Po(YIk/'$dÜ)l~|ayZ#Y\yv  &]\bU?i@ڰŒ U:lNEYv ̐z.j/wXYC~P _]\$1OGTMh ggi܅D@LWoCDCop15jgr?o Qe,=4caޮb2GEWgSe>X60<`/]ܵ5@$)tt1'Cv?m8_8>drk]/zN^#hE;C(ϸ&!Au!Mnj?|٦ )>"hXP= G&hwٻVM7pO'ms]c*?*eLX{unHNI; SBXitKCC149so1kaNlJY]86]Igtﯶ%hʍK}¡N$4"Gg Ee-jlo"gZq5NR7L{X٢>r}gD go)sXs<ԃHe)HֱQ.uZDspwa`?S`#>N6:aS=%>ו29jP4jTj> -[G2 ǀۏBdsI񙄧oJf/J禟v PaQAPSz,%K+.E? Z!(ƠU-V6$wo@ezy~Ev42+]D烴> 4JD{tr.юCd%QDp_YIV;F6M&衷Ҁ r^P1ѡP SJOoH'e34d"mK9yF{زbK;اb%c ߗAF *6Bg{4[kM8Wkgu?dy {|7-& +8J2K7f5fё/Xgx& GT1d W~"uX(.hMv-jJ:]"IeepJQFMʜtt#$Ra!wf#  Qȿ%dfL#~jVkCZs(ͮ0v2GY,.tf]TX58a,]ܮ3I:)O MlK`/$\2vܞFD>\pB*P/ endstream endobj 108 0 obj << /Type /FontDescriptor /FontName /MRPSRV+SFRM1440 /Flags 4 /FontBBox [-178 -319 1370 944] /Ascent 689 /CapHeight 689 /Descent -194 /ItalicAngle 0 /StemV 50 /XHeight 430 /CharSet (/H/R/b/c/e/eacute/eight/h/m/one/p/r/s/t/two/u/y/zero) /FontFile 107 0 R >> endobj 109 0 obj << /Length1 721 /Length2 12104 /Length3 0 /Length 12682 /Filter /FlateDecode >> stream xmwct%ܲmܱNǶٱgǶm۶ݱNұ;;o?Yc͚~"'9xJr,Ll&Vxrr-H Pvf 3<9@ @eLOfmhbacPu06𹸸8:388 0-RN@5 )%/WHA@Ck1@rRLmvƶ qrd @NL廸 @LQE`2J-99El鿖-Z:bgfX;f x&2p;ll_BjH 0E;[[T"6vN@ P1ehcaBAfcwQ hhWjSCW]&6؂ʿms!"dPv -==!i0/g`fbx0_'=3' fg;;8AN 4_]5 Lm )+6koՍn]ҬH}Aٔ3IXCr 5P7Gl\" + I{yuPΕE0%'y}jEyK/'rg~WOW(Ju=Q/cI-PF)V}U$7a3{r{h ?ĨkmU0f8,# rmͯi^Ȣl-RVSsYU}=u.t_=HF 2 Hyÿm'7P0 E:Vvcݍ#σQAoe; aoVp@ӕ(-j5`}7(0w-2^ֵƛpKE8͋?|4L`8~I}kI+`,dtk48Nׅ/;h6Pǯl^AD+#oke>ꗲH[PJh=RkTϊU|.?76`(InC|m,TL>:Ef'!8}(Kֈ9|f0`&u`},0~Efd289)J^yLJtwaD> WYzy*V|ZI?ç߂ϏV39;[l^ԳSc$6zn~}_ ;09"%HZ>?^޵r n6b8\Y߇LYyLFT KXD?O?!L\?[dS)ۛPuCcLP7ނ+Q0a%y$18y_ưԮd:),o|:h˯I1LٻGmjl;4z.pАr/^Ȫ3etӫL#"쁯qҧ-[nsA(/ ԨBU8kE~5*ж$ 4S"x"`A-!++1Z(P;o"qY38tUoIQ߅{巷S)B{nyW.˷л?`a&Τ G{  x%BOqurhQռ?]60] G!*<(Fְj,+guRgbm[?ng2jh <2m@ 6ccWoԺm'"Ź[J#xr-}D#[ӂ4 ^ezzL~(Xeʲ"W$zY"m/PLMWV}X*^DE.#Q뿓tegMƫ`!cM,Zpj.^pZгP0cX ՜-F:Iʔg{t؟=f}nJj4-lԹؿ0\yRD󧑰N}Y+VW{9qTK#?K.U մ"RbZnU g\4xCBOJoݍq՟U5Qh9|;WKGğB_!p=\l?j;TQ|F8/y2}G@kztOL_9#0RTkƄ.pQSwۑ?]\JBP"H׃T`v'+ZLj&yNubf3(R+iYQ)3dZb>f.E V`] [b=mƤO7Sm+$ڏ`j){Oe酎v#w!D pm`AYl4aT_hN\V0[\Yjîe1wIlˈi[.]&G?=tWġ ߫éUhu9CKhÕIbp0s[IݢgfS%ڛ84Ĩp-ݶ&cpisM < l &F#jTh"W:7R};OgLy4ط `MʧmQ™D)F"jjWԞ듖*9;[$.29v1`)(Gq-fd;mŷ暅c>sjPƙ&'ωvڞi aXVTسM9707/W9Ҳ)=ّ QvsFJmŔWK ޅnn1M5S²Y7Vly{ CuKp_':_yCwZ-sи:SdM5sX% nj쉁0nEf &IȄ \p'L'[X¤* Dɜqz$~'IIrxϩOKчɂB0֙H=anm L׋,Gdƪ`CEF<"?%Pk9z`ugp-jXHRMz+f)l`Zo An V?~H&MFI0٪cEg`zXag& SrV7 Q7~yڹ$Kf[wMɩHU-ӎMG# G>^jãRʐ;b`]0aج4unA=+DۜC;Bxs1M6PȜK#{濋|{!AGx?}qy#q.4du&VBI9~jgE TIXK^df%H7_*stU'+:D" 64ѱƺBcHVT0%.NaK q?!KZ^#h>m֛)Z8Mi/}b.r5 ; @(Q9@wQUh:@[t6]TVZ'e- zLq.nPyڤ-HrDNh+ q vWom%yDQ%_#᭛˘Q Ƴ i20s!F;mZ!FLK!޾TۃEБ^fH۠f՝gOU-Qz2I…Ms^"!Ov_<=Fq JyU@Ybixe'+̫bo?!=et{&K%' mXc6ߠ@Afr%Q4|l_3^祥ylE"Q,%rqԑGCH,V.by4drZqtn"TϿܢe\O۫ES^n>KPbv43HoPd:dvRI'ź5:#j!#t-8HIy߬N:w/mӟ^uٵ/.)RuL8yQ@X5%d!o/B5po] ^1]J' usQ \AmjLڰm >$"t(f΁OwT (Ij"Ol4_T&:wE;W1I"w^gy!/~iլbtygbkyo鷐6)(^vXe%H ł~Ve-q0U>884E;w_gɛGb:q2^f1pDRU!k)m%xR )8tT瀙'ZbR}W>0-.yz& o1c$r5,#1KKop#zؽIw#}^R7W}*͋g?4!Dk җhwo7arο*V/=0'Y.-" TR윔`~HhvX| (颩$y]!pT޿"o,GjHL`4ͷ:.u#h s!J"&K0?zEl3QÒٵyF̶%] @[u7t˥Ʌo;ӊҤhzhn]%G51qoc>ٵhx3x$Z܆ MJK)2M6}fd^*̰x$HfK !S`!B~"[J@N~8OT^Jg5=R]MgJ>j]Kf VOq4f H}&qPvs5+s+ClPie~'ՌON9ZjYVbPվcUjfWE2QEBקF:0I;&<_2j9V` i9*ܢy墣VOz39(~OqfVsN[ =!qgvDܛ CQ|k^P YH]S^bKW7bkswZH#*P*ަ} ̬VkQB$q?[.Ew-Cq(f̀Y1o)܆s,)B ̢.$W$#ၠ?WWT A֪K>/Rh*ٶ:qhJ;:rmP4y!^K,۫o@sū𜵿 ډ ~{jJ~&Ǹ+M71]B -%,;^#gDOیܷX;XKX0ו<2o/C a'P\?9E~g Jг{/%<" eۿD\?eG٘SP L$p}H~фi˦p[ȽeUt5|k3M9=}ۧD}C^dS.qg T6XSH'tkƟk筴D4Ds+@Nwd5^55LIapo3^a6s L^⛱\ɵ0jOYS3e^,_Dž/u9*l~FMW2L}l=~Vꪫ+!@~ l7r@jxxF]NO&4ԯf@qn4,{qڞSJN+Tgsʨ+@/% endstream endobj 110 0 obj << /Type /FontDescriptor /FontName /RBELUP+SFRM2074 /Flags 4 /FontBBox [-170 -318 1322 952] /Ascent 689 /CapHeight 689 /Descent -194 /ItalicAngle 0 /StemV 50 /XHeight 430 /CharSet (/D/E/Q/S/a/c/colon/e/four/i/p/q/r/s/t/two/u/v/x/zero) /FontFile 109 0 R >> endobj 111 0 obj << /Length1 726 /Length2 1716 /Length3 0 /Length 2288 /Filter /FlateDecode >> stream xmRy<&Qg'l23$YǒA֨1x5C9,.{-("(TRM|~s}=ss 05q8Mm $s 6's@ N8Kx.~hk]qi`/IIzƙ=5)%*,$)cjMa>;:7$]hv0sGRܻ-l1ї鵧B-CѶ--<(,|>j9`0!y}#SA/;|.s!ABܧ̯ .9َDZ=if((L{ BK~BD<1:^:ˆlzTx@6%0K@/MF]"/4>"`+f>:`v)$#v-' m~,Z3XBƹ* X(Qn(VR|y1#cҾ5ar8FS#K1 9HfW#T?5y YYj9AN'3GV^IFzNV+[X))ʾBox8qNcw[Ǿiq0c1@};~۸ikKWANg#6iAnS^yёb;#+#Mה'7-1Y!n3N/}FmnPKЗ8jqo ׾Ib‹o?A6kV:v,+b "ձz![[خ.=0Qs˖H P ' E5uo['#'h Ge>hM~6^&a:)M-fgZ5԰:K jwGz^ fmy.2 Ķ$B!k]0"i9FmWʎ nۯ㊶9 1n{C Of=0Nt*Q:#FlIq%F/xt\pl>~낽YY <_Osg9ts6u_uU'ښw"Ȧ'p1hαGNORoY e'pژa=!y >tngkF% KDrT[l;%1MצMY"RfycYDz^E}v&{p g墝kZ__%C߻%,g{ 7 > endobj 76 0 obj << /Type /Encoding /Differences [21/endash 27/ff/fi 30/ffi 37/percent 39/quoteright 44/comma/hyphen/period 48/zero/one/two/three/four/five/six/seven/eight/nine/colon/semicolon 65/A 67/C/D/E/F/G/H/I 76/L/M/N/O/P/Q/R/S/T 91/bracketleft 97/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v 120/x/y 201/Eacute 224/agrave 232/egrave/eacute 247/oe] >> endobj 63 0 obj << /Type /Font /Subtype /Type1 /BaseFont /YQJSDJ+CMEX10 /FontDescriptor 86 0 R /FirstChar 88 /LastChar 88 /Widths 72 0 R >> endobj 56 0 obj << /Type /Font /Subtype /Type1 /BaseFont /CWEMXH+CMMI12 /FontDescriptor 88 0 R /FirstChar 13 /LastChar 120 /Widths 74 0 R >> endobj 62 0 obj << /Type /Font /Subtype /Type1 /BaseFont /JEOAOO+CMMI8 /FontDescriptor 90 0 R /FirstChar 97 /LastChar 120 /Widths 73 0 R >> endobj 55 0 obj << /Type /Font /Subtype /Type1 /BaseFont /ISQPKL+CMR12 /FontDescriptor 92 0 R /FirstChar 22 /LastChar 91 /Widths 75 0 R >> endobj 64 0 obj << /Type /Font /Subtype /Type1 /BaseFont /BRJPEZ+CMR8 /FontDescriptor 94 0 R /FirstChar 49 /LastChar 61 /Widths 71 0 R >> endobj 65 0 obj << /Type /Font /Subtype /Type1 /BaseFont /SVEOTC+CMSY10 /FontDescriptor 96 0 R /FirstChar 0 /LastChar 112 /Widths 70 0 R >> endobj 44 0 obj << /Type /Font /Subtype /Type1 /BaseFont /XAQESQ+SFBX1200 /FontDescriptor 98 0 R /FirstChar 27 /LastChar 233 /Widths 80 0 R /Encoding 76 0 R >> endobj 43 0 obj << /Type /Font /Subtype /Type1 /BaseFont /ZCDABV+SFBX1440 /FontDescriptor 100 0 R /FirstChar 46 /LastChar 233 /Widths 81 0 R /Encoding 76 0 R >> endobj 42 0 obj << /Type /Font /Subtype /Type1 /BaseFont /TOSHNU+SFBX1728 /FontDescriptor 102 0 R /FirstChar 49 /LastChar 201 /Widths 82 0 R /Encoding 76 0 R >> endobj 46 0 obj << /Type /Font /Subtype /Type1 /BaseFont /DPNCDM+SFCC1200 /FontDescriptor 104 0 R /FirstChar 46 /LastChar 105 /Widths 78 0 R /Encoding 76 0 R >> endobj 45 0 obj << /Type /Font /Subtype /Type1 /BaseFont /EKNYIJ+SFRM1200 /FontDescriptor 106 0 R /FirstChar 21 /LastChar 247 /Widths 79 0 R /Encoding 76 0 R >> endobj 41 0 obj << /Type /Font /Subtype /Type1 /BaseFont /MRPSRV+SFRM1440 /FontDescriptor 108 0 R /FirstChar 48 /LastChar 233 /Widths 83 0 R /Encoding 76 0 R >> endobj 40 0 obj << /Type /Font /Subtype /Type1 /BaseFont /RBELUP+SFRM2074 /FontDescriptor 110 0 R /FirstChar 48 /LastChar 120 /Widths 84 0 R /Encoding 76 0 R >> endobj 48 0 obj << /Type /Font /Subtype /Type1 /BaseFont /RGQNCB+SFTI1200 /FontDescriptor 112 0 R /FirstChar 100 /LastChar 111 /Widths 77 0 R /Encoding 76 0 R >> endobj 49 0 obj << /Type /Pages /Count 4 /Kids [34 0 R 51 0 R 59 0 R 67 0 R] >> endobj 113 0 obj << /Type /Outlines /First 7 0 R /Last 7 0 R /Count 1 >> endobj 31 0 obj << /Title 32 0 R /A 29 0 R /Parent 19 0 R /Prev 27 0 R >> endobj 27 0 obj << /Title 28 0 R /A 25 0 R /Parent 19 0 R /Prev 23 0 R /Next 31 0 R >> endobj 23 0 obj << /Title 24 0 R /A 21 0 R /Parent 19 0 R /Next 27 0 R >> endobj 19 0 obj << /Title 20 0 R /A 17 0 R /Parent 7 0 R /Prev 15 0 R /First 23 0 R /Last 31 0 R /Count -3 >> endobj 15 0 obj << /Title 16 0 R /A 13 0 R /Parent 7 0 R /Prev 11 0 R /Next 19 0 R >> endobj 11 0 obj << /Title 12 0 R /A 9 0 R /Parent 7 0 R /Next 15 0 R >> endobj 7 0 obj << /Title 8 0 R /A 5 0 R /Parent 113 0 R /First 11 0 R /Last 19 0 R /Count -3 >> endobj 114 0 obj << /Names [(Doc-Start) 39 0 R (figure.1) 47 0 R (figure.2) 54 0 R (page.1) 38 0 R (page.2) 53 0 R (page.3) 61 0 R] /Limits [(Doc-Start) (page.3)] >> endobj 115 0 obj << /Names [(page.4) 69 0 R (section.1) 6 0 R (subsection.1.1) 10 0 R (subsection.1.2) 14 0 R (subsection.1.3) 18 0 R (subsubsection.1.3.1) 22 0 R] /Limits [(page.4) (subsubsection.1.3.1)] >> endobj 116 0 obj << /Names [(subsubsection.1.3.2) 26 0 R (subsubsection.1.3.3) 30 0 R] /Limits [(subsubsection.1.3.2) (subsubsection.1.3.3)] >> endobj 117 0 obj << /Kids [114 0 R 115 0 R 116 0 R] /Limits [(Doc-Start) (subsubsection.1.3.3)] >> endobj 118 0 obj << /Dests 117 0 R >> endobj 119 0 obj << /Type /Catalog /Pages 49 0 R /Outlines 113 0 R /Names 118 0 R /PageMode/UseOutlines /OpenAction 33 0 R >> endobj 120 0 obj << /Author()/Title()/Subject()/Creator(LaTeX with hyperref package)/Producer(pdfTeX-1.40.3)/Keywords() /CreationDate (D:20080921130151+02'00') /ModDate (D:20080921130151+02'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.141592-1.40.3-2.2 (Web2C 7.5.6) kpathsea version 3.5.6) >> endobj xref 0 121 0000000001 65535 f 0000000002 00000 f 0000000003 00000 f 0000000004 00000 f 0000000000 00000 f 0000000015 00000 n 0000002951 00000 n 0000144434 00000 n 0000000060 00000 n 0000000110 00000 n 0000003010 00000 n 0000144362 00000 n 0000000160 00000 n 0000000198 00000 n 0000009708 00000 n 0000144276 00000 n 0000000249 00000 n 0000000295 00000 n 0000009828 00000 n 0000144166 00000 n 0000000346 00000 n 0000000395 00000 n 0000009888 00000 n 0000144092 00000 n 0000000451 00000 n 0000000506 00000 n 0000011884 00000 n 0000144005 00000 n 0000000562 00000 n 0000000608 00000 n 0000013912 00000 n 0000143931 00000 n 0000000664 00000 n 0000000704 00000 n 0000002716 00000 n 0000004174 00000 n 0000003130 00000 n 0000000754 00000 n 0000002831 00000 n 0000002891 00000 n 0000143455 00000 n 0000143294 00000 n 0000142811 00000 n 0000142650 00000 n 0000142490 00000 n 0000143133 00000 n 0000142972 00000 n 0000003070 00000 n 0000143616 00000 n 0000143778 00000 n 0000009947 00000 n 0000004059 00000 n 0000003284 00000 n 0000009648 00000 n 0000009768 00000 n 0000142073 00000 n 0000141792 00000 n 0000009088 00000 n 0000011943 00000 n 0000011709 00000 n 0000010112 00000 n 0000011824 00000 n 0000141933 00000 n 0000141652 00000 n 0000142212 00000 n 0000142350 00000 n 0000013972 00000 n 0000013737 00000 n 0000012097 00000 n 0000013852 00000 n 0000014126 00000 n 0000014775 00000 n 0000014871 00000 n 0000014896 00000 n 0000015058 00000 n 0000015700 00000 n 0000141296 00000 n 0000016093 00000 n 0000016183 00000 n 0000016552 00000 n 0000017886 00000 n 0000019143 00000 n 0000020225 00000 n 0000021159 00000 n 0000022214 00000 n 0000022654 00000 n 0000024417 00000 n 0000024648 00000 n 0000029126 00000 n 0000029381 00000 n 0000032735 00000 n 0000032966 00000 n 0000036779 00000 n 0000037093 00000 n 0000039387 00000 n 0000039626 00000 n 0000040990 00000 n 0000041222 00000 n 0000059470 00000 n 0000059806 00000 n 0000073211 00000 n 0000073497 00000 n 0000085761 00000 n 0000086019 00000 n 0000092263 00000 n 0000092491 00000 n 0000112818 00000 n 0000113263 00000 n 0000125311 00000 n 0000125584 00000 n 0000138386 00000 n 0000138659 00000 n 0000141066 00000 n 0000143858 00000 n 0000144530 00000 n 0000144696 00000 n 0000144904 00000 n 0000145048 00000 n 0000145147 00000 n 0000145185 00000 n 0000145311 00000 n trailer << /Size 121 /Root 119 0 R /Info 120 0 R /ID [<6C5F57F550C9EF80F6611251D177E208> <6C5F57F550C9EF80F6611251D177E208>] >> startxref 145625 %%EOF circus-0.12.1/examples/flask_app.py000066400000000000000000000004021256046442300172150ustar00rootroot00000000000000#import resource #resource.setrlimit(resource.RLIMIT_NOFILE, (100, 100)) from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": app.run(debug=True, port=8181, host='0.0.0.0') circus-0.12.1/examples/flask_redirect.py000066400000000000000000000007061256046442300202450ustar00rootroot00000000000000from flask import Flask, redirect, make_response app = Flask(__name__) app.debug = True @app.route("/file.pdf") def file(): with open('file.pdf', 'rb') as f: response = make_response(f.read()) response.headers['Content-Type'] = "application/pdf" return response @app.route("/") def page_redirect(): return redirect("http://localhost:8000") if __name__ == "__main__": app.run(debug=True, port=8181, host='0.0.0.0') circus-0.12.1/examples/flask_serve.py000066400000000000000000000005721256046442300175710ustar00rootroot00000000000000from flask import Flask, make_response app = Flask(__name__) app.debug = True import requests @app.route("/") def pdf(): pdf = requests.get('http://localhost:5000/file.pdf') response = make_response(pdf.content) response.headers['Content-Type'] = "application/pdf" return response if __name__ == "__main__": app.run(debug=True, port=8181, host='0.0.0.0') circus-0.12.1/examples/hang.ini000066400000000000000000000002641256046442300163270ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:dummy] cmd = ../bin/python hang.py circus-0.12.1/examples/hang.py000066400000000000000000000003521256046442300161760ustar00rootroot00000000000000import sys import StringIO from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": #sys.stderr = sys.stdout = StringIO.StringIO() app.run(port=8000) circus-0.12.1/examples/leaker.py000066400000000000000000000002371256046442300165260ustar00rootroot00000000000000# sleeps for 55555 then leaks memory import time if __name__ == '__main__': time.sleep(5) memory = '' while True: memory += 100000 * ' ' circus-0.12.1/examples/listener.py000066400000000000000000000004261256046442300171100ustar00rootroot00000000000000from circus.consumer import CircusConsumer import json ZMQ_ENDPOINT = 'tcp://127.0.0.1:5556' topic = 'show:' for message, message_topic in CircusConsumer(topic, endpoint=ZMQ_ENDPOINT): response = json.dumps(dict(message=message, topic=message_topic)) print(response) circus-0.12.1/examples/max_age.ini000066400000000000000000000004331256046442300170110ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 debug = True [watcher:dummy] cmd = python args = -u dummy_fly2.py $(circus.wid) warmup_delay = 0 numprocesses = 3 rlimit_nofile = 300 rlimit_nproc = 10 max_age = 10 max_age_variance = 5 circus-0.12.1/examples/plugin_watchdog.ini000066400000000000000000000005741256046442300205740ustar00rootroot00000000000000[circus] check_delay = 60 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 [watcher:test1] cmd = /usr/bin/python2 examples/plugin_watchdog.py [watcher:test2] cmd = sleep 120 [watcher:dummy2] cmd = sleep 120 [plugin:mywatchdog] use = circus.plugins.watchdog.WatchDog loop_rate = 4 watchers_regex = "^test.*$" msg_regex = "^(?P.*);(?P.*)$" circus-0.12.1/examples/plugin_watchdog.py000077500000000000000000000006071256046442300204450ustar00rootroot00000000000000#!/usr/bin/python import socket import time import os UDP_IP = "127.0.0.1" UDP_PORT = 1664 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP my_pid = os.getpid() for _ in range(25): message = "{pid};{time}".format(pid=my_pid, time=time.time()) print('sending:{0}'.format(message)) sock.sendto(message, (UDP_IP, UDP_PORT)) time.sleep(2) circus-0.12.1/examples/redirect_serve.ini000066400000000000000000000011651256046442300204200ustar00rootroot00000000000000[circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 httpd = True [watcher:serve] cmd = ../bin/chaussette --fd $(circus.sockets.serve) flask_serve.app use_sockets = True numprocesses = 2 stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream [socket:serve] host = 0.0.0.0 port = 8000 [watcher:redirect] cmd = ../bin/chaussette --fd $(circus.sockets.redirect) flask_redirect.app use_sockets = True numprocesses = 3 stdout_stream.class = StdoutStream stderr_stream.class = StdoutStream [socket:redirect] host = 0.0.0.0 port = 5000 circus-0.12.1/examples/simplesocket.ini000066400000000000000000000010111256046442300201030ustar00rootroot00000000000000; this is an example for a simple socket process. ; simplesocket_server.py is automatically launched by circus. ; You have to then run simplesocket_client.py multiple times yourself ; (in a terminal) [circus] check_delay = 5 endpoint = tcp://127.0.0.1:5555 pubsub_endpoint = tcp://127.0.0.1:5556 stats_endpoint = tcp://127.0.0.1:5557 [watcher:worker] cmd = python simplesocket_server.py $(circus.sockets.simplesock) use_sockets = True warmup_delay = 0 numprocesses = 3 [socket:simplesock] host = 127.0.0.1 port = 8888 circus-0.12.1/examples/simplesocket_client.py000066400000000000000000000003161256046442300213210ustar00rootroot00000000000000import socket #connect to a worker sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 8888)) data = sock.recv(100) print('Received : {0}'.format(repr(data))) sock.close() circus-0.12.1/examples/simplesocket_server.py000066400000000000000000000007461256046442300213600ustar00rootroot00000000000000import socket import sys import time import os import random fd = int(sys.argv[1]) # getting the FD from circus sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM) # By default socket created by circus is in non-blocking mode. For this example # we change this. sock.setblocking(1) random.seed() while True: conn, addr = sock.accept() conn.sendall("Hello Circus by %s" % (os.getpid(),)) seconds = random.randint(2, 12) time.sleep(seconds) conn.close() circus-0.12.1/examples/test.sh000077500000000000000000000000231256046442300162200ustar00rootroot00000000000000#!/bin/sh ;sleep 1 circus-0.12.1/examples/uwsgi_lossless_reload.ini000066400000000000000000000006231256046442300220240ustar00rootroot00000000000000[watcher:web] cmd = uwsgi --ini uwsgi.ini --socket fd://$(circus.sockets.web) --stats --stats 127.0.0.1:809$(circus.wid) stop_signal = QUIT use_sockets = True hooks.after_spawn = examples.uwsgi_lossless_reload.children_started hooks.before_signal = examples.uwsgi_lossless_reload.clean_stop hooks.extended_stats = examples.uwsgi_lossless_reload.extended_stats [socket:web] host = 127.0.0.1 port = 8888 circus-0.12.1/examples/uwsgi_lossless_reload.py000066400000000000000000000126621256046442300217030ustar00rootroot00000000000000__author__ = 'Code Cobblers, Inc.' # This is an example of how to get lossless reload of WSGI web servers with # circus and uWSGI. You will, of course, need to specify the web app you # need for uWSGI to run. # # This example also solves another problem I have faced many times with uWSGI. # When you start an app with a defect in uWSGI, uWSGI will keep restarting # it forever. So this example includes an after_spawn hook that does flapping # detection on the uWSGI workers. # # Here is the flow for a reload: # 1. You issue a reload command to the watcher # 2. The watcher starts a new instance of uWSGI # 3. The after_spawn hook ensures that the workers are not flapping and halts # the new process if it is, aborting the reload. This would leave the old # process running so that you are not just SOL. # 4. The watcher issues SIGQUIT to the old instance, which is intercepted by # the before_signal hook # 5. We send SIGTSTP to the old process to tell uWSGI to stop receiving new # connections # 6. We query the stats from the old process in a loop waiting for the old # workers to go to the pause state # 7. We return True, allowing the SIGQUIT to be issued to the old process from time import time, sleep import socket from json import loads import signal from circus import logger from circus.py3compat import PY2 import re worker_states = { 'running': "idle busy cheap".split(" "), 'paused': "pause".split(" "), } NON_JSON_CHARACTERS = re.compile(r'[\x00-\x1f\x7f-\xff]') class TimeoutError(Exception): """The operation timed out.""" def get_uwsgi_stats(name, wid, base_port): sock = socket.create_connection(('127.0.0.1', base_port + wid), timeout=1) received = sock.recv(100000) data = bytes() while received: data += received received = sock.recv(100000) if not data: logger.error( "Error: No stats seem available for WID %d of %s", wid, name) return # recent versions of uWSGI had some garbage in the JSON so strip it out if not PY2: data = data.decode('latin', 'replace') data = NON_JSON_CHARACTERS.sub('', data) return loads(data) def get_worker_states(name, wid, base_port, minimum_age=0.0): stats = get_uwsgi_stats(name, wid, base_port) if 'workers' not in stats: logger.error("Error: No workers found for WID %d of %d", wid, name) return ['unknown'] workers = stats['workers'] return [ worker["status"] if 'status' in worker and worker['last_spawn'] < time() - minimum_age else 'unknown' for worker in workers ] def wait_for_workers(name, wid, base_port, state, timeout_seconds=60, minimum_age=0): started = time() while True: try: if all(worker.lower() in worker_states[state] for worker in get_worker_states(name, wid, base_port, minimum_age)): return except Exception: if time() > started + 3: raise if timeout_seconds and time() > started + timeout_seconds: raise TimeoutError('timeout') sleep(0.25) def extended_stats(watcher, arbiter, hook_name, pid, stats, **kwargs): name = watcher.name wid = watcher.processes[pid].wid try: uwsgi_stats = get_uwsgi_stats(name, wid, int(watcher._options.get('stats_base_port', 8090))) for k in ('load', 'version'): if k in uwsgi_stats: stats[k] = uwsgi_stats[k] if 'children' in stats and 'workers' in uwsgi_stats: workers = dict((worker['pid'], worker) for worker in uwsgi_stats['workers']) for worker in stats['children']: uwsgi_worker = workers.get(worker['pid']) if uwsgi_worker: for k in ('exceptions', 'harakiri_count', 'requests', 'respawn_count', 'status', 'tx'): if k in uwsgi_worker: worker[k] = uwsgi_worker[k] except Exception: pass return True def children_started(watcher, arbiter, hook_name, pid, **kwargs): name = watcher.name wid = watcher.processes[pid].wid base_port = int(watcher._options.get('stats_base_port', 8090)) logger.info('%s waiting for workers', name) try: wait_for_workers(name, wid, base_port, 'running', timeout_seconds=15, minimum_age=5) return True except TimeoutError: logger.error('%s children are flapping on %d', name, pid) return False except Exception: logger.error('%s not publishing stats on %d', name, pid) return False def clean_stop(watcher, arbiter, hook_name, pid, signum, **kwargs): if len(watcher.processes) > watcher.numprocesses and signum == signal.SIGQUIT: name = watcher.name started = watcher.processes[pid].started newer_pids = [p for p, w in watcher.processes.items() if p != pid and w.started > started] # if the one being stopped is actually the newer one, just do it if len(newer_pids) < watcher.numprocesses: return True wid = watcher.processes[pid].wid base_port = int(watcher._options.get('stats_base_port', 8090)) logger.info('%s pausing', name) try: watcher.send_signal(pid, signal.SIGTSTP) wait_for_workers(name, wid, base_port, 'paused') logger.info('%s workers idle', name) except Exception as e: logger.error('trouble pausing %s: %s', name, e) return True circus-0.12.1/examples/verbose_fly.py000077500000000000000000000003321256046442300176010ustar00rootroot00000000000000#!/usr/bin/env python import os import time import sys i = 0 while True: #print '%d:%d' % (os.getpid(), i) sys.stdout.write('%d:%d\n' % (os.getpid(), i)) sys.stdout.flush() time.sleep(0.1) i += 1 circus-0.12.1/examples/webclient/000077500000000000000000000000001256046442300166635ustar00rootroot00000000000000circus-0.12.1/examples/webclient/bread.py000066400000000000000000000024451256046442300203170ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Bread: A Simple Web Client for Circus """ from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler import json import argparse from circus.consumer import CircusConsumer from flask import Flask, request, render_template ZMQ_ENDPOINT = 'tcp://127.0.0.1:5556' app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') @app.route('/api') def api(): """WebSocket endpoint; Takes a 'topic' GET param.""" ws = request.environ.get('wsgi.websocket') topic = request.args.get('topic') if None in (ws, topic): return topic = topic.encode('ascii') for message, message_topic in CircusConsumer(topic, endpoint=ZMQ_ENDPOINT): response = json.dumps(dict(message=message, topic=message_topic)) ws.send(response) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--host', default='127.0.0.1') parser.add_argument('--port', default=5000) args = parser.parse_args() server_loc = (args.host, args.port) print('HTTP Server running at http://%s:%s/...' % server_loc) http_server = WSGIServer(server_loc, app, handler_class=WebSocketHandler) http_server.serve_forever() if __name__ == '__main__': main() circus-0.12.1/examples/webclient/requirements.txt000066400000000000000000000001241256046442300221440ustar00rootroot00000000000000gevent flask hg+https://bitbucket.org/Jeffrey/gevent-websocket#egg=gevent-websocket circus-0.12.1/examples/webclient/static/000077500000000000000000000000001256046442300201525ustar00rootroot00000000000000circus-0.12.1/examples/webclient/static/bootstrap.css000066400000000000000000002647271256046442300227230ustar00rootroot00000000000000/*! * Bootstrap v2.0.2 * * Copyright 2012 Twitter, Inc * Licensed under the Apache License v2.0 * http://www.apache.org/licenses/LICENSE-2.0 * * Designed and built with all the love in the world @twitter by @mdo and @fat. */ article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } audio:not([controls]) { display: none; } html { font-size: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } a:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } a:hover, a:active { outline: 0; } sub, sup { position: relative; font-size: 75%; line-height: 0; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } img { height: auto; border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } button, input, select, textarea { margin: 0; font-size: 100%; vertical-align: middle; } button, input { *overflow: visible; line-height: normal; } button::-moz-focus-inner, input::-moz-focus-inner { padding: 0; border: 0; } button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; } input[type="search"] { -webkit-appearance: textfield; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; } textarea { overflow: auto; vertical-align: top; } .clearfix { *zoom: 1; } .clearfix:before, .clearfix:after { display: table; content: ""; } .clearfix:after { clear: both; } .hide-text { overflow: hidden; text-indent: 100%; white-space: nowrap; } .input-block-level { display: block; width: 100%; min-height: 28px; /* Make inputs at least the height of their button counterpart */ /* Makes inputs behave like true block-level elements */ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; } body { margin: 0; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; line-height: 18px; color: #333333; background-color: #ffffff; } a { color: #0088cc; text-decoration: none; } a:hover { color: #005580; text-decoration: underline; } .row { margin-left: -20px; *zoom: 1; } .row:before, .row:after { display: table; content: ""; } .row:after { clear: both; } [class*="span"] { float: left; margin-left: 20px; } .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { width: 940px; } .span12 { width: 940px; } .span11 { width: 860px; } .span10 { width: 780px; } .span9 { width: 700px; } .span8 { width: 620px; } .span7 { width: 540px; } .span6 { width: 460px; } .span5 { width: 380px; } .span4 { width: 300px; } .span3 { width: 220px; } .span2 { width: 140px; } .span1 { width: 60px; } .offset12 { margin-left: 980px; } .offset11 { margin-left: 900px; } .offset10 { margin-left: 820px; } .offset9 { margin-left: 740px; } .offset8 { margin-left: 660px; } .offset7 { margin-left: 580px; } .offset6 { margin-left: 500px; } .offset5 { margin-left: 420px; } .offset4 { margin-left: 340px; } .offset3 { margin-left: 260px; } .offset2 { margin-left: 180px; } .offset1 { margin-left: 100px; } .row-fluid { width: 100%; *zoom: 1; } .row-fluid:before, .row-fluid:after { display: table; content: ""; } .row-fluid:after { clear: both; } .row-fluid > [class*="span"] { float: left; margin-left: 2.127659574%; } .row-fluid > [class*="span"]:first-child { margin-left: 0; } .row-fluid > .span12 { width: 99.99999998999999%; } .row-fluid > .span11 { width: 91.489361693%; } .row-fluid > .span10 { width: 82.97872339599999%; } .row-fluid > .span9 { width: 74.468085099%; } .row-fluid > .span8 { width: 65.95744680199999%; } .row-fluid > .span7 { width: 57.446808505%; } .row-fluid > .span6 { width: 48.93617020799999%; } .row-fluid > .span5 { width: 40.425531911%; } .row-fluid > .span4 { width: 31.914893614%; } .row-fluid > .span3 { width: 23.404255317%; } .row-fluid > .span2 { width: 14.89361702%; } .row-fluid > .span1 { width: 6.382978723%; } .container { margin-left: auto; margin-right: auto; *zoom: 1; } .container:before, .container:after { display: table; content: ""; } .container:after { clear: both; } .container-fluid { padding-left: 20px; padding-right: 20px; *zoom: 1; } .container-fluid:before, .container-fluid:after { display: table; content: ""; } .container-fluid:after { clear: both; } p { margin: 0 0 9px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; line-height: 18px; } p small { font-size: 11px; color: #999999; } .lead { margin-bottom: 18px; font-size: 20px; font-weight: 200; line-height: 27px; } h1, h2, h3, h4, h5, h6 { margin: 0; font-family: inherit; font-weight: bold; color: inherit; text-rendering: optimizelegibility; } h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-weight: normal; color: #999999; } h1 { font-size: 30px; line-height: 36px; } h1 small { font-size: 18px; } h2 { font-size: 24px; line-height: 36px; } h2 small { font-size: 18px; } h3 { line-height: 27px; font-size: 18px; } h3 small { font-size: 14px; } h4, h5, h6 { line-height: 18px; } h4 { font-size: 14px; } h4 small { font-size: 12px; } h5 { font-size: 12px; } h6 { font-size: 11px; color: #999999; text-transform: uppercase; } .page-header { padding-bottom: 17px; margin: 18px 0; border-bottom: 1px solid #eeeeee; } .page-header h1 { line-height: 1; } ul, ol { padding: 0; margin: 0 0 9px 25px; } ul ul, ul ol, ol ol, ol ul { margin-bottom: 0; } ul { list-style: disc; } ol { list-style: decimal; } li { line-height: 18px; } ul.unstyled, ol.unstyled { margin-left: 0; list-style: none; } dl { margin-bottom: 18px; } dt, dd { line-height: 18px; } dt { font-weight: bold; line-height: 17px; } dd { margin-left: 9px; } .dl-horizontal dt { float: left; clear: left; width: 120px; text-align: right; } .dl-horizontal dd { margin-left: 130px; } hr { margin: 18px 0; border: 0; border-top: 1px solid #eeeeee; border-bottom: 1px solid #ffffff; } strong { font-weight: bold; } em { font-style: italic; } .muted { color: #999999; } abbr[title] { border-bottom: 1px dotted #ddd; cursor: help; } abbr.initialism { font-size: 90%; text-transform: uppercase; } blockquote { padding: 0 0 0 15px; margin: 0 0 18px; border-left: 5px solid #eeeeee; } blockquote p { margin-bottom: 0; font-size: 16px; font-weight: 300; line-height: 22.5px; } blockquote small { display: block; line-height: 18px; color: #999999; } blockquote small:before { content: '\2014 \00A0'; } blockquote.pull-right { float: right; padding-left: 0; padding-right: 15px; border-left: 0; border-right: 5px solid #eeeeee; } blockquote.pull-right p, blockquote.pull-right small { text-align: right; } q:before, q:after, blockquote:before, blockquote:after { content: ""; } address { display: block; margin-bottom: 18px; line-height: 18px; font-style: normal; } small { font-size: 100%; } cite { font-style: normal; } code, pre { padding: 0 3px 2px; font-family: Menlo, Monaco, "Courier New", monospace; font-size: 12px; color: #333333; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } code { padding: 2px 4px; color: #d14; background-color: #f7f7f9; border: 1px solid #e1e1e8; } pre { display: block; padding: 8.5px; margin: 0 0 9px; font-size: 12.025px; line-height: 18px; background-color: #f5f5f5; border: 1px solid #ccc; border: 1px solid rgba(0, 0, 0, 0.15); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; white-space: pre; white-space: pre-wrap; word-break: break-all; word-wrap: break-word; } pre.prettyprint { margin-bottom: 18px; } pre code { padding: 0; color: inherit; background-color: transparent; border: 0; } .pre-scrollable { max-height: 340px; overflow-y: scroll; } form { margin: 0 0 18px; } fieldset { padding: 0; margin: 0; border: 0; } legend { display: block; width: 100%; padding: 0; margin-bottom: 27px; font-size: 19.5px; line-height: 36px; color: #333333; border: 0; border-bottom: 1px solid #eee; } legend small { font-size: 13.5px; color: #999999; } label, input, button, select, textarea { font-size: 13px; font-weight: normal; line-height: 18px; } input, button, select, textarea { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } label { display: block; margin-bottom: 5px; color: #333333; } input, textarea, select, .uneditable-input { display: inline-block; width: 210px; height: 18px; padding: 4px; margin-bottom: 9px; font-size: 13px; line-height: 18px; color: #555555; border: 1px solid #cccccc; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .uneditable-textarea { width: auto; height: auto; } label input, label textarea, label select { display: block; } input[type="image"], input[type="checkbox"], input[type="radio"] { width: auto; height: auto; padding: 0; margin: 3px 0; *margin-top: 0; /* IE7 */ line-height: normal; cursor: pointer; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; border: 0 \9; /* IE9 and down */ } input[type="image"] { border: 0; } input[type="file"] { width: auto; padding: initial; line-height: initial; border: initial; background-color: #ffffff; background-color: initial; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } input[type="button"], input[type="reset"], input[type="submit"] { width: auto; height: auto; } select, input[type="file"] { height: 28px; /* In IE7, the height of the select element cannot be changed by height, only font-size */ *margin-top: 4px; /* For IE7, add top margin to align select with labels */ line-height: 28px; } input[type="file"] { line-height: 18px \9; } select { width: 220px; background-color: #ffffff; } select[multiple], select[size] { height: auto; } input[type="image"] { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } textarea { height: auto; } input[type="hidden"] { display: none; } .radio, .checkbox { padding-left: 18px; } .radio input[type="radio"], .checkbox input[type="checkbox"] { float: left; margin-left: -18px; } .controls > .radio:first-child, .controls > .checkbox:first-child { padding-top: 5px; } .radio.inline, .checkbox.inline { display: inline-block; padding-top: 5px; margin-bottom: 0; vertical-align: middle; } .radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline { margin-left: 10px; } input, textarea { -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; -moz-transition: border linear 0.2s, box-shadow linear 0.2s; -ms-transition: border linear 0.2s, box-shadow linear 0.2s; -o-transition: border linear 0.2s, box-shadow linear 0.2s; transition: border linear 0.2s, box-shadow linear 0.2s; } input:focus, textarea:focus { border-color: rgba(82, 168, 236, 0.8); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); outline: 0; outline: thin dotted \9; /* IE6-9 */ } input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus, select:focus { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .input-mini { width: 60px; } .input-small { width: 90px; } .input-medium { width: 150px; } .input-large { width: 210px; } .input-xlarge { width: 270px; } .input-xxlarge { width: 530px; } input[class*="span"], select[class*="span"], textarea[class*="span"], .uneditable-input { float: none; margin-left: 0; } input, textarea, .uneditable-input { margin-left: 0; } input.span12, textarea.span12, .uneditable-input.span12 { width: 930px; } input.span11, textarea.span11, .uneditable-input.span11 { width: 850px; } input.span10, textarea.span10, .uneditable-input.span10 { width: 770px; } input.span9, textarea.span9, .uneditable-input.span9 { width: 690px; } input.span8, textarea.span8, .uneditable-input.span8 { width: 610px; } input.span7, textarea.span7, .uneditable-input.span7 { width: 530px; } input.span6, textarea.span6, .uneditable-input.span6 { width: 450px; } input.span5, textarea.span5, .uneditable-input.span5 { width: 370px; } input.span4, textarea.span4, .uneditable-input.span4 { width: 290px; } input.span3, textarea.span3, .uneditable-input.span3 { width: 210px; } input.span2, textarea.span2, .uneditable-input.span2 { width: 130px; } input.span1, textarea.span1, .uneditable-input.span1 { width: 50px; } input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { background-color: #eeeeee; border-color: #ddd; cursor: not-allowed; } .control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline { color: #c09853; } .control-group.warning input, .control-group.warning select, .control-group.warning textarea { color: #c09853; border-color: #c09853; } .control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { border-color: #a47e3c; -webkit-box-shadow: 0 0 6px #dbc59e; -moz-box-shadow: 0 0 6px #dbc59e; box-shadow: 0 0 6px #dbc59e; } .control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on { color: #c09853; background-color: #fcf8e3; border-color: #c09853; } .control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline { color: #b94a48; } .control-group.error input, .control-group.error select, .control-group.error textarea { color: #b94a48; border-color: #b94a48; } .control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus { border-color: #953b39; -webkit-box-shadow: 0 0 6px #d59392; -moz-box-shadow: 0 0 6px #d59392; box-shadow: 0 0 6px #d59392; } .control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on { color: #b94a48; background-color: #f2dede; border-color: #b94a48; } .control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline { color: #468847; } .control-group.success input, .control-group.success select, .control-group.success textarea { color: #468847; border-color: #468847; } .control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus { border-color: #356635; -webkit-box-shadow: 0 0 6px #7aba7b; -moz-box-shadow: 0 0 6px #7aba7b; box-shadow: 0 0 6px #7aba7b; } .control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on { color: #468847; background-color: #dff0d8; border-color: #468847; } input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { color: #b94a48; border-color: #ee5f5b; } input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { border-color: #e9322d; -webkit-box-shadow: 0 0 6px #f8b9b7; -moz-box-shadow: 0 0 6px #f8b9b7; box-shadow: 0 0 6px #f8b9b7; } .form-actions { padding: 17px 20px 18px; margin-top: 18px; margin-bottom: 18px; background-color: #eeeeee; border-top: 1px solid #ddd; *zoom: 1; } .form-actions:before, .form-actions:after { display: table; content: ""; } .form-actions:after { clear: both; } .uneditable-input { display: block; background-color: #ffffff; border-color: #eee; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); cursor: not-allowed; } :-moz-placeholder { color: #999999; } ::-webkit-input-placeholder { color: #999999; } .help-block, .help-inline { color: #555555; } .help-block { display: block; margin-bottom: 9px; } .help-inline { display: inline-block; *display: inline; /* IE7 inline-block hack */ *zoom: 1; vertical-align: middle; padding-left: 5px; } .input-prepend, .input-append { margin-bottom: 5px; } .input-prepend input, .input-append input, .input-prepend select, .input-append select, .input-prepend .uneditable-input, .input-append .uneditable-input { *margin-left: 0; -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .input-prepend input:focus, .input-append input:focus, .input-prepend select:focus, .input-append select:focus, .input-prepend .uneditable-input:focus, .input-append .uneditable-input:focus { position: relative; z-index: 2; } .input-prepend .uneditable-input, .input-append .uneditable-input { border-left-color: #ccc; } .input-prepend .add-on, .input-append .add-on { display: inline-block; width: auto; min-width: 16px; height: 18px; padding: 4px 5px; font-weight: normal; line-height: 18px; text-align: center; text-shadow: 0 1px 0 #ffffff; vertical-align: middle; background-color: #eeeeee; border: 1px solid #ccc; } .input-prepend .add-on, .input-append .add-on, .input-prepend .btn, .input-append .btn { -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .input-prepend .active, .input-append .active { background-color: #a9dba9; border-color: #46a546; } .input-prepend .add-on, .input-prepend .btn { margin-right: -1px; } .input-append input, .input-append select .uneditable-input { -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .input-append .uneditable-input { border-left-color: #eee; border-right-color: #ccc; } .input-append .add-on, .input-append .btn { margin-left: -1px; -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .input-prepend.input-append input, .input-prepend.input-append select, .input-prepend.input-append .uneditable-input { -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .input-prepend.input-append .add-on:first-child, .input-prepend.input-append .btn:first-child { margin-right: -1px; -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .input-prepend.input-append .add-on:last-child, .input-prepend.input-append .btn:last-child { margin-left: -1px; -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .search-query { padding-left: 14px; padding-right: 14px; margin-bottom: 0; -webkit-border-radius: 14px; -moz-border-radius: 14px; border-radius: 14px; } .form-search input, .form-inline input, .form-horizontal input, .form-search textarea, .form-inline textarea, .form-horizontal textarea, .form-search select, .form-inline select, .form-horizontal select, .form-search .help-inline, .form-inline .help-inline, .form-horizontal .help-inline, .form-search .uneditable-input, .form-inline .uneditable-input, .form-horizontal .uneditable-input, .form-search .input-prepend, .form-inline .input-prepend, .form-horizontal .input-prepend, .form-search .input-append, .form-inline .input-append, .form-horizontal .input-append { display: inline-block; margin-bottom: 0; } .form-search .hide, .form-inline .hide, .form-horizontal .hide { display: none; } .form-search label, .form-inline label { display: inline-block; } .form-search .input-append, .form-inline .input-append, .form-search .input-prepend, .form-inline .input-prepend { margin-bottom: 0; } .form-search .radio, .form-search .checkbox, .form-inline .radio, .form-inline .checkbox { padding-left: 0; margin-bottom: 0; vertical-align: middle; } .form-search .radio input[type="radio"], .form-search .checkbox input[type="checkbox"], .form-inline .radio input[type="radio"], .form-inline .checkbox input[type="checkbox"] { float: left; margin-left: 0; margin-right: 3px; } .control-group { margin-bottom: 9px; } legend + .control-group { margin-top: 18px; -webkit-margin-top-collapse: separate; } .form-horizontal .control-group { margin-bottom: 18px; *zoom: 1; } .form-horizontal .control-group:before, .form-horizontal .control-group:after { display: table; content: ""; } .form-horizontal .control-group:after { clear: both; } .form-horizontal .control-label { float: left; width: 140px; padding-top: 5px; text-align: right; } .form-horizontal .controls { margin-left: 160px; /* Super jank IE7 fix to ensure the inputs in .input-append and input-prepend don't inherit the margin of the parent, in this case .controls */ *display: inline-block; *margin-left: 0; *padding-left: 20px; } .form-horizontal .help-block { margin-top: 9px; margin-bottom: 0; } .form-horizontal .form-actions { padding-left: 160px; } table { max-width: 100%; border-collapse: collapse; border-spacing: 0; background-color: transparent; } .table { width: 100%; margin-bottom: 18px; } .table th, .table td { padding: 8px; line-height: 18px; text-align: left; vertical-align: top; border-top: 1px solid #dddddd; } .table th { font-weight: bold; } .table thead th { vertical-align: bottom; } .table colgroup + thead tr:first-child th, .table colgroup + thead tr:first-child td, .table thead:first-child tr:first-child th, .table thead:first-child tr:first-child td { border-top: 0; } .table tbody + tbody { border-top: 2px solid #dddddd; } .table-condensed th, .table-condensed td { padding: 4px 5px; } .table-bordered { border: 1px solid #dddddd; border-left: 0; border-collapse: separate; *border-collapse: collapsed; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .table-bordered th, .table-bordered td { border-left: 1px solid #dddddd; } .table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { border-top: 0; } .table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { -webkit-border-radius: 4px 0 0 0; -moz-border-radius: 4px 0 0 0; border-radius: 4px 0 0 0; } .table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { -webkit-border-radius: 0 4px 0 0; -moz-border-radius: 0 4px 0 0; border-radius: 0 4px 0 0; } .table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { -webkit-border-radius: 0 0 0 4px; -moz-border-radius: 0 0 0 4px; border-radius: 0 0 0 4px; } .table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { -webkit-border-radius: 0 0 4px 0; -moz-border-radius: 0 0 4px 0; border-radius: 0 0 4px 0; } .table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { background-color: #f9f9f9; } .table tbody tr:hover td, .table tbody tr:hover th { background-color: #f5f5f5; } table .span1 { float: none; width: 44px; margin-left: 0; } table .span2 { float: none; width: 124px; margin-left: 0; } table .span3 { float: none; width: 204px; margin-left: 0; } table .span4 { float: none; width: 284px; margin-left: 0; } table .span5 { float: none; width: 364px; margin-left: 0; } table .span6 { float: none; width: 444px; margin-left: 0; } table .span7 { float: none; width: 524px; margin-left: 0; } table .span8 { float: none; width: 604px; margin-left: 0; } table .span9 { float: none; width: 684px; margin-left: 0; } table .span10 { float: none; width: 764px; margin-left: 0; } table .span11 { float: none; width: 844px; margin-left: 0; } table .span12 { float: none; width: 924px; margin-left: 0; } table .span13 { float: none; width: 1004px; margin-left: 0; } table .span14 { float: none; width: 1084px; margin-left: 0; } table .span15 { float: none; width: 1164px; margin-left: 0; } table .span16 { float: none; width: 1244px; margin-left: 0; } table .span17 { float: none; width: 1324px; margin-left: 0; } table .span18 { float: none; width: 1404px; margin-left: 0; } table .span19 { float: none; width: 1484px; margin-left: 0; } table .span20 { float: none; width: 1564px; margin-left: 0; } table .span21 { float: none; width: 1644px; margin-left: 0; } table .span22 { float: none; width: 1724px; margin-left: 0; } table .span23 { float: none; width: 1804px; margin-left: 0; } table .span24 { float: none; width: 1884px; margin-left: 0; } [class^="icon-"], [class*=" icon-"] { display: inline-block; width: 14px; height: 14px; line-height: 14px; vertical-align: text-top; background-image: url("../img/glyphicons-halflings.png"); background-position: 14px 14px; background-repeat: no-repeat; *margin-right: .3em; } [class^="icon-"]:last-child, [class*=" icon-"]:last-child { *margin-left: 0; } .icon-white { background-image: url("../img/glyphicons-halflings-white.png"); } .icon-glass { background-position: 0 0; } .icon-music { background-position: -24px 0; } .icon-search { background-position: -48px 0; } .icon-envelope { background-position: -72px 0; } .icon-heart { background-position: -96px 0; } .icon-star { background-position: -120px 0; } .icon-star-empty { background-position: -144px 0; } .icon-user { background-position: -168px 0; } .icon-film { background-position: -192px 0; } .icon-th-large { background-position: -216px 0; } .icon-th { background-position: -240px 0; } .icon-th-list { background-position: -264px 0; } .icon-ok { background-position: -288px 0; } .icon-remove { background-position: -312px 0; } .icon-zoom-in { background-position: -336px 0; } .icon-zoom-out { background-position: -360px 0; } .icon-off { background-position: -384px 0; } .icon-signal { background-position: -408px 0; } .icon-cog { background-position: -432px 0; } .icon-trash { background-position: -456px 0; } .icon-home { background-position: 0 -24px; } .icon-file { background-position: -24px -24px; } .icon-time { background-position: -48px -24px; } .icon-road { background-position: -72px -24px; } .icon-download-alt { background-position: -96px -24px; } .icon-download { background-position: -120px -24px; } .icon-upload { background-position: -144px -24px; } .icon-inbox { background-position: -168px -24px; } .icon-play-circle { background-position: -192px -24px; } .icon-repeat { background-position: -216px -24px; } .icon-refresh { background-position: -240px -24px; } .icon-list-alt { background-position: -264px -24px; } .icon-lock { background-position: -287px -24px; } .icon-flag { background-position: -312px -24px; } .icon-headphones { background-position: -336px -24px; } .icon-volume-off { background-position: -360px -24px; } .icon-volume-down { background-position: -384px -24px; } .icon-volume-up { background-position: -408px -24px; } .icon-qrcode { background-position: -432px -24px; } .icon-barcode { background-position: -456px -24px; } .icon-tag { background-position: 0 -48px; } .icon-tags { background-position: -25px -48px; } .icon-book { background-position: -48px -48px; } .icon-bookmark { background-position: -72px -48px; } .icon-print { background-position: -96px -48px; } .icon-camera { background-position: -120px -48px; } .icon-font { background-position: -144px -48px; } .icon-bold { background-position: -167px -48px; } .icon-italic { background-position: -192px -48px; } .icon-text-height { background-position: -216px -48px; } .icon-text-width { background-position: -240px -48px; } .icon-align-left { background-position: -264px -48px; } .icon-align-center { background-position: -288px -48px; } .icon-align-right { background-position: -312px -48px; } .icon-align-justify { background-position: -336px -48px; } .icon-list { background-position: -360px -48px; } .icon-indent-left { background-position: -384px -48px; } .icon-indent-right { background-position: -408px -48px; } .icon-facetime-video { background-position: -432px -48px; } .icon-picture { background-position: -456px -48px; } .icon-pencil { background-position: 0 -72px; } .icon-map-marker { background-position: -24px -72px; } .icon-adjust { background-position: -48px -72px; } .icon-tint { background-position: -72px -72px; } .icon-edit { background-position: -96px -72px; } .icon-share { background-position: -120px -72px; } .icon-check { background-position: -144px -72px; } .icon-move { background-position: -168px -72px; } .icon-step-backward { background-position: -192px -72px; } .icon-fast-backward { background-position: -216px -72px; } .icon-backward { background-position: -240px -72px; } .icon-play { background-position: -264px -72px; } .icon-pause { background-position: -288px -72px; } .icon-stop { background-position: -312px -72px; } .icon-forward { background-position: -336px -72px; } .icon-fast-forward { background-position: -360px -72px; } .icon-step-forward { background-position: -384px -72px; } .icon-eject { background-position: -408px -72px; } .icon-chevron-left { background-position: -432px -72px; } .icon-chevron-right { background-position: -456px -72px; } .icon-plus-sign { background-position: 0 -96px; } .icon-minus-sign { background-position: -24px -96px; } .icon-remove-sign { background-position: -48px -96px; } .icon-ok-sign { background-position: -72px -96px; } .icon-question-sign { background-position: -96px -96px; } .icon-info-sign { background-position: -120px -96px; } .icon-screenshot { background-position: -144px -96px; } .icon-remove-circle { background-position: -168px -96px; } .icon-ok-circle { background-position: -192px -96px; } .icon-ban-circle { background-position: -216px -96px; } .icon-arrow-left { background-position: -240px -96px; } .icon-arrow-right { background-position: -264px -96px; } .icon-arrow-up { background-position: -289px -96px; } .icon-arrow-down { background-position: -312px -96px; } .icon-share-alt { background-position: -336px -96px; } .icon-resize-full { background-position: -360px -96px; } .icon-resize-small { background-position: -384px -96px; } .icon-plus { background-position: -408px -96px; } .icon-minus { background-position: -433px -96px; } .icon-asterisk { background-position: -456px -96px; } .icon-exclamation-sign { background-position: 0 -120px; } .icon-gift { background-position: -24px -120px; } .icon-leaf { background-position: -48px -120px; } .icon-fire { background-position: -72px -120px; } .icon-eye-open { background-position: -96px -120px; } .icon-eye-close { background-position: -120px -120px; } .icon-warning-sign { background-position: -144px -120px; } .icon-plane { background-position: -168px -120px; } .icon-calendar { background-position: -192px -120px; } .icon-random { background-position: -216px -120px; } .icon-comment { background-position: -240px -120px; } .icon-magnet { background-position: -264px -120px; } .icon-chevron-up { background-position: -288px -120px; } .icon-chevron-down { background-position: -313px -119px; } .icon-retweet { background-position: -336px -120px; } .icon-shopping-cart { background-position: -360px -120px; } .icon-folder-close { background-position: -384px -120px; } .icon-folder-open { background-position: -408px -120px; } .icon-resize-vertical { background-position: -432px -119px; } .icon-resize-horizontal { background-position: -456px -118px; } .dropdown { position: relative; } .dropdown-toggle { *margin-bottom: -3px; } .dropdown-toggle:active, .open .dropdown-toggle { outline: 0; } .caret { display: inline-block; width: 0; height: 0; vertical-align: top; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid #000000; opacity: 0.3; filter: alpha(opacity=30); content: ""; } .dropdown .caret { margin-top: 8px; margin-left: 2px; } .dropdown:hover .caret, .open.dropdown .caret { opacity: 1; filter: alpha(opacity=100); } .dropdown-menu { position: absolute; top: 100%; left: 0; z-index: 1000; float: left; display: none; min-width: 160px; padding: 4px 0; margin: 0; list-style: none; background-color: #ffffff; border-color: #ccc; border-color: rgba(0, 0, 0, 0.2); border-style: solid; border-width: 1px; -webkit-border-radius: 0 0 5px 5px; -moz-border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px; -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -webkit-background-clip: padding-box; -moz-background-clip: padding; background-clip: padding-box; *border-right-width: 2px; *border-bottom-width: 2px; } .dropdown-menu.pull-right { right: 0; left: auto; } .dropdown-menu .divider { height: 1px; margin: 8px 1px; overflow: hidden; background-color: #e5e5e5; border-bottom: 1px solid #ffffff; *width: 100%; *margin: -5px 0 5px; } .dropdown-menu a { display: block; padding: 3px 15px; clear: both; font-weight: normal; line-height: 18px; color: #333333; white-space: nowrap; } .dropdown-menu li > a:hover, .dropdown-menu .active > a, .dropdown-menu .active > a:hover { color: #ffffff; text-decoration: none; background-color: #0088cc; } .dropdown.open { *z-index: 1000; } .dropdown.open .dropdown-toggle { color: #ffffff; background: #ccc; background: rgba(0, 0, 0, 0.3); } .dropdown.open .dropdown-menu { display: block; } .pull-right .dropdown-menu { left: auto; right: 0; } .dropup .caret, .navbar-fixed-bottom .dropdown .caret { border-top: 0; border-bottom: 4px solid #000000; content: "\2191"; } .dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu { top: auto; bottom: 100%; margin-bottom: 1px; } .typeahead { margin-top: 2px; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .well { min-height: 20px; padding: 19px; margin-bottom: 20px; background-color: #f5f5f5; border: 1px solid #eee; border: 1px solid rgba(0, 0, 0, 0.05); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); } .well blockquote { border-color: #ddd; border-color: rgba(0, 0, 0, 0.15); } .well-large { padding: 24px; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; } .well-small { padding: 9px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .fade { -webkit-transition: opacity 0.15s linear; -moz-transition: opacity 0.15s linear; -ms-transition: opacity 0.15s linear; -o-transition: opacity 0.15s linear; transition: opacity 0.15s linear; opacity: 0; } .fade.in { opacity: 1; } .collapse { -webkit-transition: height 0.35s ease; -moz-transition: height 0.35s ease; -ms-transition: height 0.35s ease; -o-transition: height 0.35s ease; transition: height 0.35s ease; position: relative; overflow: hidden; height: 0; } .collapse.in { height: auto; } .close { float: right; font-size: 20px; font-weight: bold; line-height: 18px; color: #000000; text-shadow: 0 1px 0 #ffffff; opacity: 0.2; filter: alpha(opacity=20); } .close:hover { color: #000000; text-decoration: none; opacity: 0.4; filter: alpha(opacity=40); cursor: pointer; } .btn { display: inline-block; *display: inline; /* IE7 inline-block hack */ *zoom: 1; padding: 4px 10px 4px; margin-bottom: 0; font-size: 13px; line-height: 18px; color: #333333; text-align: center; text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); vertical-align: middle; background-color: #f5f5f5; background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); background-image: linear-gradient(top, #ffffff, #e6e6e6); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); border-color: #e6e6e6 #e6e6e6 #bfbfbf; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); border: 1px solid #cccccc; border-bottom-color: #b3b3b3; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); cursor: pointer; *margin-left: .3em; } .btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] { background-color: #e6e6e6; } .btn:active, .btn.active { background-color: #cccccc \9; } .btn:first-child { *margin-left: 0; } .btn:hover { color: #333333; text-decoration: none; background-color: #e6e6e6; background-position: 0 -15px; -webkit-transition: background-position 0.1s linear; -moz-transition: background-position 0.1s linear; -ms-transition: background-position 0.1s linear; -o-transition: background-position 0.1s linear; transition: background-position 0.1s linear; } .btn:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; outline-offset: -2px; } .btn.active, .btn:active { background-image: none; -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); background-color: #e6e6e6; background-color: #d9d9d9 \9; outline: 0; } .btn.disabled, .btn[disabled] { cursor: default; background-image: none; background-color: #e6e6e6; opacity: 0.65; filter: alpha(opacity=65); -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } .btn-large { padding: 9px 14px; font-size: 15px; line-height: normal; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .btn-large [class^="icon-"] { margin-top: 1px; } .btn-small { padding: 5px 9px; font-size: 11px; line-height: 16px; } .btn-small [class^="icon-"] { margin-top: -1px; } .btn-mini { padding: 2px 6px; font-size: 11px; line-height: 14px; } .btn-primary, .btn-primary:hover, .btn-warning, .btn-warning:hover, .btn-danger, .btn-danger:hover, .btn-success, .btn-success:hover, .btn-info, .btn-info:hover, .btn-inverse, .btn-inverse:hover { text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); color: #ffffff; } .btn-primary.active, .btn-warning.active, .btn-danger.active, .btn-success.active, .btn-info.active, .btn-inverse.active { color: rgba(255, 255, 255, 0.75); } .btn-primary { background-color: #0074cc; background-image: -moz-linear-gradient(top, #0088cc, #0055cc); background-image: -ms-linear-gradient(top, #0088cc, #0055cc); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc)); background-image: -webkit-linear-gradient(top, #0088cc, #0055cc); background-image: -o-linear-gradient(top, #0088cc, #0055cc); background-image: linear-gradient(top, #0088cc, #0055cc); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0); border-color: #0055cc #0055cc #003580; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] { background-color: #0055cc; } .btn-primary:active, .btn-primary.active { background-color: #004099 \9; } .btn-warning { background-color: #faa732; background-image: -moz-linear-gradient(top, #fbb450, #f89406); background-image: -ms-linear-gradient(top, #fbb450, #f89406); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); background-image: -webkit-linear-gradient(top, #fbb450, #f89406); background-image: -o-linear-gradient(top, #fbb450, #f89406); background-image: linear-gradient(top, #fbb450, #f89406); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); border-color: #f89406 #f89406 #ad6704; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-warning:hover, .btn-warning:active, .btn-warning.active, .btn-warning.disabled, .btn-warning[disabled] { background-color: #f89406; } .btn-warning:active, .btn-warning.active { background-color: #c67605 \9; } .btn-danger { background-color: #da4f49; background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); background-image: linear-gradient(top, #ee5f5b, #bd362f); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); border-color: #bd362f #bd362f #802420; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-danger:hover, .btn-danger:active, .btn-danger.active, .btn-danger.disabled, .btn-danger[disabled] { background-color: #bd362f; } .btn-danger:active, .btn-danger.active { background-color: #942a25 \9; } .btn-success { background-color: #5bb75b; background-image: -moz-linear-gradient(top, #62c462, #51a351); background-image: -ms-linear-gradient(top, #62c462, #51a351); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); background-image: -webkit-linear-gradient(top, #62c462, #51a351); background-image: -o-linear-gradient(top, #62c462, #51a351); background-image: linear-gradient(top, #62c462, #51a351); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); border-color: #51a351 #51a351 #387038; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-success:hover, .btn-success:active, .btn-success.active, .btn-success.disabled, .btn-success[disabled] { background-color: #51a351; } .btn-success:active, .btn-success.active { background-color: #408140 \9; } .btn-info { background-color: #49afcd; background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); background-image: linear-gradient(top, #5bc0de, #2f96b4); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); border-color: #2f96b4 #2f96b4 #1f6377; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-info:hover, .btn-info:active, .btn-info.active, .btn-info.disabled, .btn-info[disabled] { background-color: #2f96b4; } .btn-info:active, .btn-info.active { background-color: #24748c \9; } .btn-inverse { background-color: #414141; background-image: -moz-linear-gradient(top, #555555, #222222); background-image: -ms-linear-gradient(top, #555555, #222222); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222)); background-image: -webkit-linear-gradient(top, #555555, #222222); background-image: -o-linear-gradient(top, #555555, #222222); background-image: linear-gradient(top, #555555, #222222); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0); border-color: #222222 #222222 #000000; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); } .btn-inverse:hover, .btn-inverse:active, .btn-inverse.active, .btn-inverse.disabled, .btn-inverse[disabled] { background-color: #222222; } .btn-inverse:active, .btn-inverse.active { background-color: #080808 \9; } button.btn, input[type="submit"].btn { *padding-top: 2px; *padding-bottom: 2px; } button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { padding: 0; border: 0; } button.btn.btn-large, input[type="submit"].btn.btn-large { *padding-top: 7px; *padding-bottom: 7px; } button.btn.btn-small, input[type="submit"].btn.btn-small { *padding-top: 3px; *padding-bottom: 3px; } button.btn.btn-mini, input[type="submit"].btn.btn-mini { *padding-top: 1px; *padding-bottom: 1px; } .btn-group { position: relative; *zoom: 1; *margin-left: .3em; } .btn-group:before, .btn-group:after { display: table; content: ""; } .btn-group:after { clear: both; } .btn-group:first-child { *margin-left: 0; } .btn-group + .btn-group { margin-left: 5px; } .btn-toolbar { margin-top: 9px; margin-bottom: 9px; } .btn-toolbar .btn-group { display: inline-block; *display: inline; /* IE7 inline-block hack */ *zoom: 1; } .btn-group .btn { position: relative; float: left; margin-left: -1px; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .btn-group .btn:first-child { margin-left: 0; -webkit-border-top-left-radius: 4px; -moz-border-radius-topleft: 4px; border-top-left-radius: 4px; -webkit-border-bottom-left-radius: 4px; -moz-border-radius-bottomleft: 4px; border-bottom-left-radius: 4px; } .btn-group .btn:last-child, .btn-group .dropdown-toggle { -webkit-border-top-right-radius: 4px; -moz-border-radius-topright: 4px; border-top-right-radius: 4px; -webkit-border-bottom-right-radius: 4px; -moz-border-radius-bottomright: 4px; border-bottom-right-radius: 4px; } .btn-group .btn.large:first-child { margin-left: 0; -webkit-border-top-left-radius: 6px; -moz-border-radius-topleft: 6px; border-top-left-radius: 6px; -webkit-border-bottom-left-radius: 6px; -moz-border-radius-bottomleft: 6px; border-bottom-left-radius: 6px; } .btn-group .btn.large:last-child, .btn-group .large.dropdown-toggle { -webkit-border-top-right-radius: 6px; -moz-border-radius-topright: 6px; border-top-right-radius: 6px; -webkit-border-bottom-right-radius: 6px; -moz-border-radius-bottomright: 6px; border-bottom-right-radius: 6px; } .btn-group .btn:hover, .btn-group .btn:focus, .btn-group .btn:active, .btn-group .btn.active { z-index: 2; } .btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { outline: 0; } .btn-group .dropdown-toggle { padding-left: 8px; padding-right: 8px; -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); *padding-top: 3px; *padding-bottom: 3px; } .btn-group .btn-mini.dropdown-toggle { padding-left: 5px; padding-right: 5px; *padding-top: 1px; *padding-bottom: 1px; } .btn-group .btn-small.dropdown-toggle { *padding-top: 4px; *padding-bottom: 4px; } .btn-group .btn-large.dropdown-toggle { padding-left: 12px; padding-right: 12px; } .btn-group.open { *z-index: 1000; } .btn-group.open .dropdown-menu { display: block; margin-top: 1px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .btn-group.open .dropdown-toggle { background-image: none; -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); } .btn .caret { margin-top: 7px; margin-left: 0; } .btn:hover .caret, .open.btn-group .caret { opacity: 1; filter: alpha(opacity=100); } .btn-mini .caret { margin-top: 5px; } .btn-small .caret { margin-top: 6px; } .btn-large .caret { margin-top: 6px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #000000; } .btn-primary .caret, .btn-warning .caret, .btn-danger .caret, .btn-info .caret, .btn-success .caret, .btn-inverse .caret { border-top-color: #ffffff; border-bottom-color: #ffffff; opacity: 0.75; filter: alpha(opacity=75); } .alert { padding: 8px 35px 8px 14px; margin-bottom: 18px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); background-color: #fcf8e3; border: 1px solid #fbeed5; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; color: #c09853; } .alert-heading { color: inherit; } .alert .close { position: relative; top: -2px; right: -21px; line-height: 18px; } .alert-success { background-color: #dff0d8; border-color: #d6e9c6; color: #468847; } .alert-danger, .alert-error { background-color: #f2dede; border-color: #eed3d7; color: #b94a48; } .alert-info { background-color: #d9edf7; border-color: #bce8f1; color: #3a87ad; } .alert-block { padding-top: 14px; padding-bottom: 14px; } .alert-block > p, .alert-block > ul { margin-bottom: 0; } .alert-block p + p { margin-top: 5px; } .nav { margin-left: 0; margin-bottom: 18px; list-style: none; } .nav > li > a { display: block; } .nav > li > a:hover { text-decoration: none; background-color: #eeeeee; } .nav .nav-header { display: block; padding: 3px 15px; font-size: 11px; font-weight: bold; line-height: 18px; color: #999999; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-transform: uppercase; } .nav li + .nav-header { margin-top: 9px; } .nav-list { padding-left: 15px; padding-right: 15px; margin-bottom: 0; } .nav-list > li > a, .nav-list .nav-header { margin-left: -15px; margin-right: -15px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); } .nav-list > li > a { padding: 3px 15px; } .nav-list > .active > a, .nav-list > .active > a:hover { color: #ffffff; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); background-color: #0088cc; } .nav-list [class^="icon-"] { margin-right: 2px; } .nav-list .divider { height: 1px; margin: 8px 1px; overflow: hidden; background-color: #e5e5e5; border-bottom: 1px solid #ffffff; *width: 100%; *margin: -5px 0 5px; } .nav-tabs, .nav-pills { *zoom: 1; } .nav-tabs:before, .nav-pills:before, .nav-tabs:after, .nav-pills:after { display: table; content: ""; } .nav-tabs:after, .nav-pills:after { clear: both; } .nav-tabs > li, .nav-pills > li { float: left; } .nav-tabs > li > a, .nav-pills > li > a { padding-right: 12px; padding-left: 12px; margin-right: 2px; line-height: 14px; } .nav-tabs { border-bottom: 1px solid #ddd; } .nav-tabs > li { margin-bottom: -1px; } .nav-tabs > li > a { padding-top: 8px; padding-bottom: 8px; line-height: 18px; border: 1px solid transparent; -webkit-border-radius: 4px 4px 0 0; -moz-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; } .nav-tabs > li > a:hover { border-color: #eeeeee #eeeeee #dddddd; } .nav-tabs > .active > a, .nav-tabs > .active > a:hover { color: #555555; background-color: #ffffff; border: 1px solid #ddd; border-bottom-color: transparent; cursor: default; } .nav-pills > li > a { padding-top: 8px; padding-bottom: 8px; margin-top: 2px; margin-bottom: 2px; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; } .nav-pills > .active > a, .nav-pills > .active > a:hover { color: #ffffff; background-color: #0088cc; } .nav-stacked > li { float: none; } .nav-stacked > li > a { margin-right: 0; } .nav-tabs.nav-stacked { border-bottom: 0; } .nav-tabs.nav-stacked > li > a { border: 1px solid #ddd; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .nav-tabs.nav-stacked > li:first-child > a { -webkit-border-radius: 4px 4px 0 0; -moz-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0; } .nav-tabs.nav-stacked > li:last-child > a { -webkit-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px; } .nav-tabs.nav-stacked > li > a:hover { border-color: #ddd; z-index: 2; } .nav-pills.nav-stacked > li > a { margin-bottom: 3px; } .nav-pills.nav-stacked > li:last-child > a { margin-bottom: 1px; } .nav-tabs .dropdown-menu, .nav-pills .dropdown-menu { margin-top: 1px; border-width: 1px; } .nav-pills .dropdown-menu { -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .nav-tabs .dropdown-toggle .caret, .nav-pills .dropdown-toggle .caret { border-top-color: #0088cc; border-bottom-color: #0088cc; margin-top: 6px; } .nav-tabs .dropdown-toggle:hover .caret, .nav-pills .dropdown-toggle:hover .caret { border-top-color: #005580; border-bottom-color: #005580; } .nav-tabs .active .dropdown-toggle .caret, .nav-pills .active .dropdown-toggle .caret { border-top-color: #333333; border-bottom-color: #333333; } .nav > .dropdown.active > a:hover { color: #000000; cursor: pointer; } .nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, .nav > .open.active > a:hover { color: #ffffff; background-color: #999999; border-color: #999999; } .nav .open .caret, .nav .open.active .caret, .nav .open a:hover .caret { border-top-color: #ffffff; border-bottom-color: #ffffff; opacity: 1; filter: alpha(opacity=100); } .tabs-stacked .open > a:hover { border-color: #999999; } .tabbable { *zoom: 1; } .tabbable:before, .tabbable:after { display: table; content: ""; } .tabbable:after { clear: both; } .tab-content { display: table; width: 100%; } .tabs-below .nav-tabs, .tabs-right .nav-tabs, .tabs-left .nav-tabs { border-bottom: 0; } .tab-content > .tab-pane, .pill-content > .pill-pane { display: none; } .tab-content > .active, .pill-content > .active { display: block; } .tabs-below .nav-tabs { border-top: 1px solid #ddd; } .tabs-below .nav-tabs > li { margin-top: -1px; margin-bottom: 0; } .tabs-below .nav-tabs > li > a { -webkit-border-radius: 0 0 4px 4px; -moz-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px; } .tabs-below .nav-tabs > li > a:hover { border-bottom-color: transparent; border-top-color: #ddd; } .tabs-below .nav-tabs .active > a, .tabs-below .nav-tabs .active > a:hover { border-color: transparent #ddd #ddd #ddd; } .tabs-left .nav-tabs > li, .tabs-right .nav-tabs > li { float: none; } .tabs-left .nav-tabs > li > a, .tabs-right .nav-tabs > li > a { min-width: 74px; margin-right: 0; margin-bottom: 3px; } .tabs-left .nav-tabs { float: left; margin-right: 19px; border-right: 1px solid #ddd; } .tabs-left .nav-tabs > li > a { margin-right: -1px; -webkit-border-radius: 4px 0 0 4px; -moz-border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px; } .tabs-left .nav-tabs > li > a:hover { border-color: #eeeeee #dddddd #eeeeee #eeeeee; } .tabs-left .nav-tabs .active > a, .tabs-left .nav-tabs .active > a:hover { border-color: #ddd transparent #ddd #ddd; *border-right-color: #ffffff; } .tabs-right .nav-tabs { float: right; margin-left: 19px; border-left: 1px solid #ddd; } .tabs-right .nav-tabs > li > a { margin-left: -1px; -webkit-border-radius: 0 4px 4px 0; -moz-border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0; } .tabs-right .nav-tabs > li > a:hover { border-color: #eeeeee #eeeeee #eeeeee #dddddd; } .tabs-right .nav-tabs .active > a, .tabs-right .nav-tabs .active > a:hover { border-color: #ddd #ddd #ddd transparent; *border-left-color: #ffffff; } .navbar { *position: relative; *z-index: 2; overflow: visible; margin-bottom: 18px; } .navbar-inner { padding-left: 20px; padding-right: 20px; background-color: #2c2c2c; background-image: -moz-linear-gradient(top, #333333, #222222); background-image: -ms-linear-gradient(top, #333333, #222222); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); background-image: -webkit-linear-gradient(top, #333333, #222222); background-image: -o-linear-gradient(top, #333333, #222222); background-image: linear-gradient(top, #333333, #222222); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); } .navbar .container { width: auto; } .btn-navbar { display: none; float: right; padding: 7px 10px; margin-left: 5px; margin-right: 5px; background-color: #2c2c2c; background-image: -moz-linear-gradient(top, #333333, #222222); background-image: -ms-linear-gradient(top, #333333, #222222); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); background-image: -webkit-linear-gradient(top, #333333, #222222); background-image: -o-linear-gradient(top, #333333, #222222); background-image: linear-gradient(top, #333333, #222222); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); border-color: #222222 #222222 #000000; border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); filter: progid:dximagetransform.microsoft.gradient(enabled=false); -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); } .btn-navbar:hover, .btn-navbar:active, .btn-navbar.active, .btn-navbar.disabled, .btn-navbar[disabled] { background-color: #222222; } .btn-navbar:active, .btn-navbar.active { background-color: #080808 \9; } .btn-navbar .icon-bar { display: block; width: 18px; height: 2px; background-color: #f5f5f5; -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); } .btn-navbar .icon-bar + .icon-bar { margin-top: 3px; } .nav-collapse.collapse { height: auto; } .navbar { color: #999999; } .navbar .brand:hover { text-decoration: none; } .navbar .brand { float: left; display: block; padding: 8px 20px 12px; margin-left: -20px; font-size: 20px; font-weight: 200; line-height: 1; color: #ffffff; } .navbar .navbar-text { margin-bottom: 0; line-height: 40px; } .navbar .btn, .navbar .btn-group { margin-top: 5px; } .navbar .btn-group .btn { margin-top: 0; } .navbar-form { margin-bottom: 0; *zoom: 1; } .navbar-form:before, .navbar-form:after { display: table; content: ""; } .navbar-form:after { clear: both; } .navbar-form input, .navbar-form select, .navbar-form .radio, .navbar-form .checkbox { margin-top: 5px; } .navbar-form input, .navbar-form select { display: inline-block; margin-bottom: 0; } .navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { margin-top: 3px; } .navbar-form .input-append, .navbar-form .input-prepend { margin-top: 6px; white-space: nowrap; } .navbar-form .input-append input, .navbar-form .input-prepend input { margin-top: 0; } .navbar-search { position: relative; float: left; margin-top: 6px; margin-bottom: 0; } .navbar-search .search-query { padding: 4px 9px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; font-weight: normal; line-height: 1; color: #ffffff; background-color: #626262; border: 1px solid #151515; -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); -webkit-transition: none; -moz-transition: none; -ms-transition: none; -o-transition: none; transition: none; } .navbar-search .search-query:-moz-placeholder { color: #cccccc; } .navbar-search .search-query::-webkit-input-placeholder { color: #cccccc; } .navbar-search .search-query:focus, .navbar-search .search-query.focused { padding: 5px 10px; color: #333333; text-shadow: 0 1px 0 #ffffff; background-color: #ffffff; border: 0; -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); outline: 0; } .navbar-fixed-top, .navbar-fixed-bottom { position: fixed; right: 0; left: 0; z-index: 1030; margin-bottom: 0; } .navbar-fixed-top .navbar-inner, .navbar-fixed-bottom .navbar-inner { padding-left: 0; padding-right: 0; -webkit-border-radius: 0; -moz-border-radius: 0; border-radius: 0; } .navbar-fixed-top .container, .navbar-fixed-bottom .container { width: 940px; } .navbar-fixed-top { top: 0; } .navbar-fixed-bottom { bottom: 0; } .navbar .nav { position: relative; left: 0; display: block; float: left; margin: 0 10px 0 0; } .navbar .nav.pull-right { float: right; } .navbar .nav > li { display: block; float: left; } .navbar .nav > li > a { float: none; padding: 10px 10px 11px; line-height: 19px; color: #999999; text-decoration: none; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); } .navbar .nav > li > a:hover { background-color: transparent; color: #ffffff; text-decoration: none; } .navbar .nav .active > a, .navbar .nav .active > a:hover { color: #ffffff; text-decoration: none; background-color: #222222; } .navbar .divider-vertical { height: 40px; width: 1px; margin: 0 9px; overflow: hidden; background-color: #222222; border-right: 1px solid #333333; } .navbar .nav.pull-right { margin-left: 10px; margin-right: 0; } .navbar .dropdown-menu { margin-top: 1px; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .navbar .dropdown-menu:before { content: ''; display: inline-block; border-left: 7px solid transparent; border-right: 7px solid transparent; border-bottom: 7px solid #ccc; border-bottom-color: rgba(0, 0, 0, 0.2); position: absolute; top: -7px; left: 9px; } .navbar .dropdown-menu:after { content: ''; display: inline-block; border-left: 6px solid transparent; border-right: 6px solid transparent; border-bottom: 6px solid #ffffff; position: absolute; top: -6px; left: 10px; } .navbar-fixed-bottom .dropdown-menu:before { border-top: 7px solid #ccc; border-top-color: rgba(0, 0, 0, 0.2); border-bottom: 0; bottom: -7px; top: auto; } .navbar-fixed-bottom .dropdown-menu:after { border-top: 6px solid #ffffff; border-bottom: 0; bottom: -6px; top: auto; } .navbar .nav .dropdown-toggle .caret, .navbar .nav .open.dropdown .caret { border-top-color: #ffffff; border-bottom-color: #ffffff; } .navbar .nav .active .caret { opacity: 1; filter: alpha(opacity=100); } .navbar .nav .open > .dropdown-toggle, .navbar .nav .active > .dropdown-toggle, .navbar .nav .open.active > .dropdown-toggle { background-color: transparent; } .navbar .nav .active > .dropdown-toggle:hover { color: #ffffff; } .navbar .nav.pull-right .dropdown-menu, .navbar .nav .dropdown-menu.pull-right { left: auto; right: 0; } .navbar .nav.pull-right .dropdown-menu:before, .navbar .nav .dropdown-menu.pull-right:before { left: auto; right: 12px; } .navbar .nav.pull-right .dropdown-menu:after, .navbar .nav .dropdown-menu.pull-right:after { left: auto; right: 13px; } .breadcrumb { padding: 7px 14px; margin: 0 0 18px; list-style: none; background-color: #fbfbfb; background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); background-image: linear-gradient(top, #ffffff, #f5f5f5); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); border: 1px solid #ddd; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; } .breadcrumb li { display: inline-block; *display: inline; /* IE7 inline-block hack */ *zoom: 1; text-shadow: 0 1px 0 #ffffff; } .breadcrumb .divider { padding: 0 5px; color: #999999; } .breadcrumb .active a { color: #333333; } .pagination { height: 36px; margin: 18px 0; } .pagination ul { display: inline-block; *display: inline; /* IE7 inline-block hack */ *zoom: 1; margin-left: 0; margin-bottom: 0; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .pagination li { display: inline; } .pagination a { float: left; padding: 0 14px; line-height: 34px; text-decoration: none; border: 1px solid #ddd; border-left-width: 0; } .pagination a:hover, .pagination .active a { background-color: #f5f5f5; } .pagination .active a { color: #999999; cursor: default; } .pagination .disabled span, .pagination .disabled a, .pagination .disabled a:hover { color: #999999; background-color: transparent; cursor: default; } .pagination li:first-child a { border-left-width: 1px; -webkit-border-radius: 3px 0 0 3px; -moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; } .pagination li:last-child a { -webkit-border-radius: 0 3px 3px 0; -moz-border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0; } .pagination-centered { text-align: center; } .pagination-right { text-align: right; } .pager { margin-left: 0; margin-bottom: 18px; list-style: none; text-align: center; *zoom: 1; } .pager:before, .pager:after { display: table; content: ""; } .pager:after { clear: both; } .pager li { display: inline; } .pager a { display: inline-block; padding: 5px 14px; background-color: #fff; border: 1px solid #ddd; -webkit-border-radius: 15px; -moz-border-radius: 15px; border-radius: 15px; } .pager a:hover { text-decoration: none; background-color: #f5f5f5; } .pager .next a { float: right; } .pager .previous a { float: left; } .pager .disabled a, .pager .disabled a:hover { color: #999999; background-color: #fff; cursor: default; } .modal-open .dropdown-menu { z-index: 2050; } .modal-open .dropdown.open { *z-index: 2050; } .modal-open .popover { z-index: 2060; } .modal-open .tooltip { z-index: 2070; } .modal-backdrop { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 1040; background-color: #000000; } .modal-backdrop.fade { opacity: 0; } .modal-backdrop, .modal-backdrop.fade.in { opacity: 0.8; filter: alpha(opacity=80); } .modal { position: fixed; top: 50%; left: 50%; z-index: 1050; overflow: auto; width: 560px; margin: -250px 0 0 -280px; background-color: #ffffff; border: 1px solid #999; border: 1px solid rgba(0, 0, 0, 0.3); *border: 1px solid #999; /* IE6-7 */ -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -webkit-background-clip: padding-box; -moz-background-clip: padding-box; background-clip: padding-box; } .modal.fade { -webkit-transition: opacity .3s linear, top .3s ease-out; -moz-transition: opacity .3s linear, top .3s ease-out; -ms-transition: opacity .3s linear, top .3s ease-out; -o-transition: opacity .3s linear, top .3s ease-out; transition: opacity .3s linear, top .3s ease-out; top: -25%; } .modal.fade.in { top: 50%; } .modal-header { padding: 9px 15px; border-bottom: 1px solid #eee; } .modal-header .close { margin-top: 2px; } .modal-body { overflow-y: auto; max-height: 400px; padding: 15px; } .modal-form { margin-bottom: 0; } .modal-footer { padding: 14px 15px 15px; margin-bottom: 0; text-align: right; background-color: #f5f5f5; border-top: 1px solid #ddd; -webkit-border-radius: 0 0 6px 6px; -moz-border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px; -webkit-box-shadow: inset 0 1px 0 #ffffff; -moz-box-shadow: inset 0 1px 0 #ffffff; box-shadow: inset 0 1px 0 #ffffff; *zoom: 1; } .modal-footer:before, .modal-footer:after { display: table; content: ""; } .modal-footer:after { clear: both; } .modal-footer .btn + .btn { margin-left: 5px; margin-bottom: 0; } .modal-footer .btn-group .btn + .btn { margin-left: -1px; } .tooltip { position: absolute; z-index: 1020; display: block; visibility: visible; padding: 5px; font-size: 11px; opacity: 0; filter: alpha(opacity=0); } .tooltip.in { opacity: 0.8; filter: alpha(opacity=80); } .tooltip.top { margin-top: -2px; } .tooltip.right { margin-left: 2px; } .tooltip.bottom { margin-top: 2px; } .tooltip.left { margin-left: -2px; } .tooltip.top .tooltip-arrow { bottom: 0; left: 50%; margin-left: -5px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #000000; } .tooltip.left .tooltip-arrow { top: 50%; right: 0; margin-top: -5px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 5px solid #000000; } .tooltip.bottom .tooltip-arrow { top: 0; left: 50%; margin-left: -5px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid #000000; } .tooltip.right .tooltip-arrow { top: 50%; left: 0; margin-top: -5px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-right: 5px solid #000000; } .tooltip-inner { max-width: 200px; padding: 3px 8px; color: #ffffff; text-align: center; text-decoration: none; background-color: #000000; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .tooltip-arrow { position: absolute; width: 0; height: 0; } .popover { position: absolute; top: 0; left: 0; z-index: 1010; display: none; padding: 5px; } .popover.top { margin-top: -5px; } .popover.right { margin-left: 5px; } .popover.bottom { margin-top: 5px; } .popover.left { margin-left: -5px; } .popover.top .arrow { bottom: 0; left: 50%; margin-left: -5px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #000000; } .popover.right .arrow { top: 50%; left: 0; margin-top: -5px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-right: 5px solid #000000; } .popover.bottom .arrow { top: 0; left: 50%; margin-left: -5px; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 5px solid #000000; } .popover.left .arrow { top: 50%; right: 0; margin-top: -5px; border-top: 5px solid transparent; border-bottom: 5px solid transparent; border-left: 5px solid #000000; } .popover .arrow { position: absolute; width: 0; height: 0; } .popover-inner { padding: 3px; width: 280px; overflow: hidden; background: #000000; background: rgba(0, 0, 0, 0.8); -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); } .popover-title { padding: 9px 15px; line-height: 1; background-color: #f5f5f5; border-bottom: 1px solid #eee; -webkit-border-radius: 3px 3px 0 0; -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; } .popover-content { padding: 14px; background-color: #ffffff; -webkit-border-radius: 0 0 3px 3px; -moz-border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px; -webkit-background-clip: padding-box; -moz-background-clip: padding-box; background-clip: padding-box; } .popover-content p, .popover-content ul, .popover-content ol { margin-bottom: 0; } .thumbnails { margin-left: -20px; list-style: none; *zoom: 1; } .thumbnails:before, .thumbnails:after { display: table; content: ""; } .thumbnails:after { clear: both; } .thumbnails > li { float: left; margin: 0 0 18px 20px; } .thumbnail { display: block; padding: 4px; line-height: 1; border: 1px solid #ddd; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); } a.thumbnail:hover { border-color: #0088cc; -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); } .thumbnail > img { display: block; max-width: 100%; margin-left: auto; margin-right: auto; } .thumbnail .caption { padding: 9px; } .label { padding: 1px 4px 2px; font-size: 10.998px; font-weight: bold; line-height: 13px; color: #ffffff; vertical-align: middle; white-space: nowrap; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #999999; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; } .label:hover { color: #ffffff; text-decoration: none; } .label-important { background-color: #b94a48; } .label-important:hover { background-color: #953b39; } .label-warning { background-color: #f89406; } .label-warning:hover { background-color: #c67605; } .label-success { background-color: #468847; } .label-success:hover { background-color: #356635; } .label-info { background-color: #3a87ad; } .label-info:hover { background-color: #2d6987; } .label-inverse { background-color: #333333; } .label-inverse:hover { background-color: #1a1a1a; } .badge { padding: 1px 9px 2px; font-size: 12.025px; font-weight: bold; white-space: nowrap; color: #ffffff; background-color: #999999; -webkit-border-radius: 9px; -moz-border-radius: 9px; border-radius: 9px; } .badge:hover { color: #ffffff; text-decoration: none; cursor: pointer; } .badge-error { background-color: #b94a48; } .badge-error:hover { background-color: #953b39; } .badge-warning { background-color: #f89406; } .badge-warning:hover { background-color: #c67605; } .badge-success { background-color: #468847; } .badge-success:hover { background-color: #356635; } .badge-info { background-color: #3a87ad; } .badge-info:hover { background-color: #2d6987; } .badge-inverse { background-color: #333333; } .badge-inverse:hover { background-color: #1a1a1a; } @-webkit-keyframes progress-bar-stripes { from { background-position: 0 0; } to { background-position: 40px 0; } } @-moz-keyframes progress-bar-stripes { from { background-position: 0 0; } to { background-position: 40px 0; } } @-ms-keyframes progress-bar-stripes { from { background-position: 0 0; } to { background-position: 40px 0; } } @keyframes progress-bar-stripes { from { background-position: 0 0; } to { background-position: 40px 0; } } .progress { overflow: hidden; height: 18px; margin-bottom: 18px; background-color: #f7f7f7; background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); background-image: linear-gradient(top, #f5f5f5, #f9f9f9); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .progress .bar { width: 0%; height: 18px; color: #ffffff; font-size: 12px; text-align: center; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); background-color: #0e90d2; background-image: -moz-linear-gradient(top, #149bdf, #0480be); background-image: -ms-linear-gradient(top, #149bdf, #0480be); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); background-image: -webkit-linear-gradient(top, #149bdf, #0480be); background-image: -o-linear-gradient(top, #149bdf, #0480be); background-image: linear-gradient(top, #149bdf, #0480be); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; -webkit-transition: width 0.6s ease; -moz-transition: width 0.6s ease; -ms-transition: width 0.6s ease; -o-transition: width 0.6s ease; transition: width 0.6s ease; } .progress-striped .bar { background-color: #149bdf; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -webkit-background-size: 40px 40px; -moz-background-size: 40px 40px; -o-background-size: 40px 40px; background-size: 40px 40px; } .progress.active .bar { -webkit-animation: progress-bar-stripes 2s linear infinite; -moz-animation: progress-bar-stripes 2s linear infinite; animation: progress-bar-stripes 2s linear infinite; } .progress-danger .bar { background-color: #dd514c; background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); background-image: linear-gradient(top, #ee5f5b, #c43c35); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); } .progress-danger.progress-striped .bar { background-color: #ee5f5b; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-success .bar { background-color: #5eb95e; background-image: -moz-linear-gradient(top, #62c462, #57a957); background-image: -ms-linear-gradient(top, #62c462, #57a957); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); background-image: -webkit-linear-gradient(top, #62c462, #57a957); background-image: -o-linear-gradient(top, #62c462, #57a957); background-image: linear-gradient(top, #62c462, #57a957); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); } .progress-success.progress-striped .bar { background-color: #62c462; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-info .bar { background-color: #4bb1cf; background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); background-image: -o-linear-gradient(top, #5bc0de, #339bb9); background-image: linear-gradient(top, #5bc0de, #339bb9); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); } .progress-info.progress-striped .bar { background-color: #5bc0de; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .progress-warning .bar { background-color: #faa732; background-image: -moz-linear-gradient(top, #fbb450, #f89406); background-image: -ms-linear-gradient(top, #fbb450, #f89406); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); background-image: -webkit-linear-gradient(top, #fbb450, #f89406); background-image: -o-linear-gradient(top, #fbb450, #f89406); background-image: linear-gradient(top, #fbb450, #f89406); background-repeat: repeat-x; filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); } .progress-warning.progress-striped .bar { background-color: #fbb450; background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); } .accordion { margin-bottom: 18px; } .accordion-group { margin-bottom: 2px; border: 1px solid #e5e5e5; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } .accordion-heading { border-bottom: 0; } .accordion-heading .accordion-toggle { display: block; padding: 8px 15px; } .accordion-inner { padding: 9px 15px; border-top: 1px solid #e5e5e5; } .carousel { position: relative; margin-bottom: 18px; line-height: 1; } .carousel-inner { overflow: hidden; width: 100%; position: relative; } .carousel .item { display: none; position: relative; -webkit-transition: 0.6s ease-in-out left; -moz-transition: 0.6s ease-in-out left; -ms-transition: 0.6s ease-in-out left; -o-transition: 0.6s ease-in-out left; transition: 0.6s ease-in-out left; } .carousel .item > img { display: block; line-height: 1; } .carousel .active, .carousel .next, .carousel .prev { display: block; } .carousel .active { left: 0; } .carousel .next, .carousel .prev { position: absolute; top: 0; width: 100%; } .carousel .next { left: 100%; } .carousel .prev { left: -100%; } .carousel .next.left, .carousel .prev.right { left: 0; } .carousel .active.left { left: -100%; } .carousel .active.right { left: 100%; } .carousel-control { position: absolute; top: 40%; left: 15px; width: 40px; height: 40px; margin-top: -20px; font-size: 60px; font-weight: 100; line-height: 30px; color: #ffffff; text-align: center; background: #222222; border: 3px solid #ffffff; -webkit-border-radius: 23px; -moz-border-radius: 23px; border-radius: 23px; opacity: 0.5; filter: alpha(opacity=50); } .carousel-control.right { left: auto; right: 15px; } .carousel-control:hover { color: #ffffff; text-decoration: none; opacity: 0.9; filter: alpha(opacity=90); } .carousel-caption { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 15px 5px; background: #333333; background: rgba(0, 0, 0, 0.75); } .carousel-caption h4, .carousel-caption p { color: #ffffff; } .hero-unit { padding: 60px; margin-bottom: 30px; background-color: #eeeeee; -webkit-border-radius: 6px; -moz-border-radius: 6px; border-radius: 6px; } .hero-unit h1 { margin-bottom: 0; font-size: 60px; line-height: 1; color: inherit; letter-spacing: -1px; } .hero-unit p { font-size: 18px; font-weight: 200; line-height: 27px; color: inherit; } .pull-right { float: right; } .pull-left { float: left; } .hide { display: none; } .show { display: block; } .invisible { visibility: hidden; } circus-0.12.1/examples/webclient/static/bread.js000066400000000000000000000022271256046442300215700ustar00rootroot00000000000000// JavaScript Code for Bread: A Sample Circus Client // TODO: Be friendly to browsers that don't have WebSockets, or use socket.io (function (globals) { var bread = {}; var host = globals.location.hostname, port = 5000; var el = document.getElementsByTagName('pre')[0]; function getSocketUrl(topic) { return ['ws://', host, ':', port, '/api?topic=', topic].join(''); } function log(msg) { var args = Array.prototype.slice.call(arguments, 0), code = document.createElement('code'); code.textContent = msg; el.appendChild(code); } bread.subscribe = function (topic) { var endpoint = getSocketUrl(topic), socket = new globals.WebSocket(endpoint); socket.onopen = function (e) { console.log('open'); log('Listening at ' + endpoint + '...'); }; socket.onmessage = function (e) { console.log('message'); log(e.data); }; socket.onclose = function (e) { console.log('closed'); log('Socket closed.'); }; }; globals.bread = bread; }(this)); circus-0.12.1/examples/webclient/static/circus.gif000066400000000000000000000146641256046442300221440ustar00rootroot00000000000000GIF89a$IIIiii@@@000---ccc>>>nnn+++ ZZZlll555NNN:::PPPrrr<<<***333pppfffBBBLLLRRRDDD ddd GGGxxxvvvtttXXX###%%%UUU(((___О]]]\\\```р^^^~~~aaa˜|||}}}ʮ{{{Ӭ!,$@GLl2SH_HG4WXƘIO$N֪4J׽JF$FH QFFP=g-e rPNH䡬1A( KB.+io3)CQ\'=}JU-f!WK ?E(!5gQ2`\8U2%MV$UVS+Y8_l)@c n+!!-p H&?TV)Y*X`36ѓ!暝|[ahN$ea ۽iP*>M$-F0d覃AD!5T ԱឧME Bѕ>~Ik(Lګ z4|(=}5@}q0"GH|ݓpl.!s)@7Wl/Ek2$,1Q`)S#!ŞƕW$?dE>YQY6t,9 )RvX2ԠG:<M͙ *ACEs0FѴ6@iT&U&#]f;8ʀP-6owP.]Ops-i? EJgD[XF|PCvp*΄^-]i@vk6ҧPP1%D>rocwwX{2FB80O~_x@E*mt0- )@BIF'Ю6IO,e)("ErC$ld ^1 #0:}N!O0(l`ƶڸR0RHQT&Y@  4 A+H5C%8pdCI> =!fjf@Ix6@P7 eJ&=^ =$!|A