tsung-1.8.0/0000755000201100017670000000000014377757020012361 5ustar nniclausdreamtsung-1.8.0/ebin/0000755000201100017670000000000014377757020013276 5ustar nniclausdreamtsung-1.8.0/tsung-recorder.sh.in0000644000201100017670000001136514377756736016310 0ustar nniclausdream#!/usr/bin/env bash UNAME=`uname` case $UNAME in "Linux") HOST=`hostname -s`;; "SunOS") HOST=`hostname`;; *) HOST=`hostname -s`;; esac INSTALL_DIR=@EXPANDED_LIBDIR@/tsung/ ERL=@ERL@ MAIN_DIR=$HOME/.tsung LOG_DIR=$MAIN_DIR/log LOG_OPT="log_file \"$LOG_DIR/tsung.log\"" VERSION=@PACKAGE_VERSION@ NAMETYPE="-sname" LISTEN_PORT=8090 USE_PARENT_PROXY=false PGSQL_SERVER_IP=127.0.0.1 PGSQL_SERVER_PORT=5432 NAME=tsung RECORDER=tsung_recorder TSUNGPATH=$INSTALL_DIR/tsung-$VERSION/ebin RECORDERPATH=$INSTALL_DIR/tsung_recorder-$VERSION/ebin CONTROLLERPATH=$INSTALL_DIR/tsung_controller-$VERSION/ebin CONF_OPT_FILE="$HOME/.tsung/tsung.xml" DEBUG_LEVEL=5 RECORDER_PLUGIN="http" ERL_RSH=" -rsh ssh " ERL_OPTS=" -smp auto +P 250000 +A 16 +K true @ERL_OPTS@ " COOKIE='tsung' stop() { $ERL $ERL_OPTS $ERL_RSH -noshell $NAMETYPE killer -setcookie $COOKIE -pa $TSUNGPATH -pa $RECORDERPATH -s tsung_recorder stop_all $HOST -s init stop RET=$? if [ $RET == 1 ]; then echo "FAILED" else echo "[OK]" rm $PIDFILE fi } status() { PIDFILE="/tmp/tsung_recorder.pid" if [ -f $PIDFILE ]; then echo "Tsung recorder started [OK]" else echo "Tsung recorder not started " fi } start() { echo "Starting Tsung recorder on port $LISTEN_PORT" $ERL $ERL_OPTS $ERL_RSH -noshell $NAMETYPE $RECORDER -setcookie $COOKIE \ -s tsung_recorder \ -pa $TSUNGPATH -pa $RECORDERPATH -pa $CONTROLLERPATH \ -tsung_recorder debug_level $DEBUG_LEVEL \ -tsung_recorder $LOG_OPT \ -tsung_recorder parent_proxy $USE_PARENT_PROXY \ -tsung_recorder plugin ts_proxy_$RECORDER_PLUGIN \ -tsung_recorder proxy_log_file \"$MAIN_DIR/tsung_recorder.xml\" \ -tsung_recorder pgsql_server \"${PGSQL_SERVER_IP}\" \ -tsung_recorder pgsql_port ${PGSQL_SERVER_PORT} \ -tsung_recorder proxy_listen_port $LISTEN_PORT & echo $! > /tmp/tsung_recorder.pid } version() { echo "Tsung Recorder version $VERSION" exit 0 } checkconfig() { if [ ! -e $CONF_OPT_FILE ] then echo "Config file $CONF_OPT_FILE doesn't exist, aborting !" exit 1 fi } maindir() { if [ ! -d $MAIN_DIR ] then echo "Creating local Tsung directory $MAIN_DIR" mkdir $MAIN_DIR fi } logdir() { if [ ! -d $LOG_DIR ] then echo "Creating Tsung log directory $LOG_DIR" mkdir $LOG_DIR fi } record_tag() { shift SNAME=tsung_recordtag $ERL -noshell $NAMETYPE $SNAME -setcookie $COOKIE -pa $TSUNGPATH -pa $RECORDERPATH -run ts_proxy_recorder recordtag $HOST "$*" -s init stop } checkrunning(){ if [ -f $PIDFILE ]; then CURPID=`cat $PIDFILE` if kill -0 $CURPID 2> /dev/null ; then echo "Can't start: Tsung recorder already running !" exit 1 fi fi } usage() { prog=`basename $0` echo "Usage: $prog start|stop|restart" echo "Options:" echo " -p plugin used for the recorder" echo " available: http, pgsql,webdav (default is http)" echo " -L listening port for the recorder (default is 8090)" echo " -I for the pgsql recorder (or parent proxy): server IP" echo " (default is 127.0.0.1)" echo " -P for the pgsql recorder (or parent proxy): server port" echo " (default is 5432)" echo " -u for the http recorder: use a parent proxy" echo " -d set log level from 0 to 7 (default is 5)" echo " -v print version information and exit" echo " -h display this help and exit" exit } while getopts "hvf:p:l:d:r:i:P:L:I:u" Option do case $Option in f) CONF_OPT_FILE=$OPTARG;; l) # must add absolute path echo "$OPTARG" | grep -q "^/" RES=$? if [ "$RES" == 0 ]; then LOG_OPT="log_file \"$OPTARG\" " else LOG_OPT="log_file \"$PWD/$OPTARG\" " fi ;; d) DEBUG_LEVEL=$OPTARG;; p) RECORDER_PLUGIN=$OPTARG;; I) PGSQL_SERVER_IP=$OPTARG;; u) USE_PARENT_PROXY=true;; P) PGSQL_SERVER_PORT=$OPTARG;; L) LISTEN_PORT=$OPTARG;; v) version;; h) usage;; *) usage ;; esac done shift $(($OPTIND - 1)) case $1 in start) PIDFILE="/tmp/tsung_recorder.pid" maindir logdir checkrunning start ;; record_tag) record_tag $* ;; stop) PIDFILE="/tmp/tsung_recorder.pid" stop ;; status) status ;; restart) stop start ;; *) usage $0 ;; esac tsung-1.8.0/tsung.spec0000644000201100017670000000462014377756762014413 0ustar nniclausdream%define name tsung %define version 1.8.0 %define release 1 Name: %{name} Version: %{version} Release: %{release}%{?dist} Summary: A distributed multi-protocol load testing tool Group: Development/Tools License: GPLv2 URL: http://tsung.erlang-projects.org/ Source0: http://tsung.erlang-projects.org/dist/%{name}-%{version}.tar.gz Vendor: Process-one Packager: Nicolas Niclausse BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: erlang doxygen-latex python-sphinx texlive-titlesec texlive-framed texlive-threeparttable texlive-wrapfig Requires: erlang Requires: perl(Template) %description tsung is a distributed load testing tool. It is protocol-independent and can currently be used to stress and benchmark HTTP, Jabber/XMPP, PostgreSQL, MySQL and LDAP servers. It simulates user behaviour using an XML description file, reports many measurements in real time (statistics can be customized with transactions, and graphics generated using gnuplot). For HTTP, it supports 1.0 and 1.1, has a proxy mode to record sessions, supports GET and POST methods, Cookies, and Basic WWW-authentication. It also has support for SSL. More information is available at http://tsung.erlang-projects.org/ . %prep %setup -q %build %configure --docdir=%{_docdir}/%{name}-%{version} make %{?_smp_mflags} %install rm -rf $RPM_BUILD_ROOT make install DESTDIR=$RPM_BUILD_ROOT install -p -m 644 CHANGELOG.md CONTRIBUTORS COPYING README.md TODO \ $RPM_BUILD_ROOT%{_docdir}/%{name}-%{version}/ %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root,-) %doc %{_docdir}/%{name}-%{version}/* %{_bindir}/tsung %{_bindir}/tsung-recorder %{_bindir}/tsplot %{_libdir}/tsung %{_datadir}/tsung %{_mandir}/man1/tsung.1* %{_mandir}/man1/tsplot.1* %{_mandir}/man1/tsung-recorder.1* %changelog * Wed Sep 20 2006 Nicolas Niclausse 1.2.1-1 - update 'requires': erlang (as in fedora extra) instead of erlang-otp * Wed Apr 27 2005 Nicolas Niclausse 1.0.2-1 - new release * Thu Nov 18 2004 Nicolas Niclausse 1.0.1-1 - new release * Mon Aug 9 2004 Nicolas Niclausse 1.0-1 - new release * Mon Aug 9 2004 Nicolas Niclausse 1.0.beta7-2 - fix doc * Mon Aug 9 2004 Nicolas Niclausse 1.0.beta7-1 - initial rpm # end of file tsung-1.8.0/tsung.spec.in0000644000201100017670000000463414377756736015026 0ustar nniclausdream%define name tsung %define version @PACKAGE_VERSION@ %define release 1 Name: %{name} Version: %{version} Release: %{release}%{?dist} Summary: A distributed multi-protocol load testing tool Group: Development/Tools License: GPLv2 URL: http://tsung.erlang-projects.org/ Source0: http://tsung.erlang-projects.org/dist/%{name}-%{version}.tar.gz Vendor: Process-one Packager: Nicolas Niclausse BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) BuildRequires: erlang doxygen-latex python-sphinx texlive-titlesec texlive-framed texlive-threeparttable texlive-wrapfig Requires: erlang Requires: perl(Template) %description tsung is a distributed load testing tool. It is protocol-independent and can currently be used to stress and benchmark HTTP, Jabber/XMPP, PostgreSQL, MySQL and LDAP servers. It simulates user behaviour using an XML description file, reports many measurements in real time (statistics can be customized with transactions, and graphics generated using gnuplot). For HTTP, it supports 1.0 and 1.1, has a proxy mode to record sessions, supports GET and POST methods, Cookies, and Basic WWW-authentication. It also has support for SSL. More information is available at http://tsung.erlang-projects.org/ . %prep %setup -q %build %configure --docdir=%{_docdir}/%{name}-%{version} make %{?_smp_mflags} %install rm -rf $RPM_BUILD_ROOT make install DESTDIR=$RPM_BUILD_ROOT install -p -m 644 CHANGELOG.md CONTRIBUTORS COPYING README.md TODO \ $RPM_BUILD_ROOT%{_docdir}/%{name}-%{version}/ %clean rm -rf $RPM_BUILD_ROOT %files %defattr(-,root,root,-) %doc %{_docdir}/%{name}-%{version}/* %{_bindir}/tsung %{_bindir}/tsung-recorder %{_bindir}/tsplot %{_libdir}/tsung %{_datadir}/tsung %{_mandir}/man1/tsung.1* %{_mandir}/man1/tsplot.1* %{_mandir}/man1/tsung-recorder.1* %changelog * Wed Sep 20 2006 Nicolas Niclausse 1.2.1-1 - update 'requires': erlang (as in fedora extra) instead of erlang-otp * Wed Apr 27 2005 Nicolas Niclausse 1.0.2-1 - new release * Thu Nov 18 2004 Nicolas Niclausse 1.0.1-1 - new release * Mon Aug 9 2004 Nicolas Niclausse 1.0-1 - new release * Mon Aug 9 2004 Nicolas Niclausse 1.0.beta7-2 - fix doc * Mon Aug 9 2004 Nicolas Niclausse 1.0.beta7-1 - initial rpm # end of file tsung-1.8.0/install-sh0000755000201100017670000001272014377756736014404 0ustar nniclausdream#!/bin/sh # # install - install a program, script, or datafile # This comes from X11R5 (mit/util/scripts/install.sh). # # Copyright 1991 by the Massachusetts Institute of Technology # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation, and that the name of M.I.T. not be used in advertising or # publicity pertaining to distribution of the software without specific, # written prior permission. M.I.T. makes no representations about the # suitability of this software for any purpose. It is provided "as is" # without express or implied warranty. # # Calling this script install-sh is preferred over install.sh, to prevent # `make' implicit rules from creating a file called install from it # when there is no Makefile. # # This script is compatible with the BSD install script, but was written # from scratch. It can only install one file at a time, a restriction # shared with many OS's install programs. # set DOITPROG to echo to test this script # Don't use :- since 4.3BSD and earlier shells don't like it. doit="${DOITPROG-}" # put in absolute paths if you don't have them in your path; or use env. vars. mvprog="${MVPROG-mv}" cpprog="${CPPROG-cp}" chmodprog="${CHMODPROG-chmod}" chownprog="${CHOWNPROG-chown}" chgrpprog="${CHGRPPROG-chgrp}" stripprog="${STRIPPROG-strip}" rmprog="${RMPROG-rm}" mkdirprog="${MKDIRPROG-mkdir}" transformbasename="" transform_arg="" instcmd="$mvprog" chmodcmd="$chmodprog 0755" chowncmd="" chgrpcmd="" stripcmd="" rmcmd="$rmprog -f" mvcmd="$mvprog" src="" dst="" dir_arg="" while [ x"$1" != x ]; do case $1 in -c) instcmd="$cpprog" shift continue;; -d) dir_arg=true shift continue;; -m) chmodcmd="$chmodprog $2" shift shift continue;; -o) chowncmd="$chownprog $2" shift shift continue;; -g) chgrpcmd="$chgrpprog $2" shift shift continue;; -s) stripcmd="$stripprog" shift continue;; -t=*) transformarg=`echo $1 | sed 's/-t=//'` shift continue;; -b=*) transformbasename=`echo $1 | sed 's/-b=//'` shift continue;; *) if [ x"$src" = x ] then src=$1 else # this colon is to work around a 386BSD /bin/sh bug : dst=$1 fi shift continue;; esac done if [ x"$src" = x ] then echo "install: no input file specified" exit 1 else true fi if [ x"$dir_arg" != x ]; then dst=$src src="" if [ -d $dst ]; then instcmd=: else instcmd=mkdir fi else # Waiting for this to be detected by the "$instcmd $src $dsttmp" command # might cause directories to be created, which would be especially bad # if $src (and thus $dsttmp) contains '*'. if [ -f $src -o -d $src ] then true else echo "install: $src does not exist" exit 1 fi if [ x"$dst" = x ] then echo "install: no destination specified" exit 1 else true fi # If destination is a directory, append the input filename; if your system # does not like double slashes in filenames, you may need to add some logic if [ -d $dst ] then dst="$dst"/`basename $src` else true fi fi ## this sed command emulates the dirname command dstdir=`echo $dst | sed -e 's,[^/]*$,,;s,/$,,;s,^$,.,'` # Make sure that the destination directory exists. # this part is taken from Noah Friedman's mkinstalldirs script # Skip lots of stat calls in the usual case. if [ ! -d "$dstdir" ]; then defaultIFS=' ' IFS="${IFS-${defaultIFS}}" oIFS="${IFS}" # Some sh's can't handle IFS=/ for some reason. IFS='%' set - `echo ${dstdir} | sed -e 's@/@%@g' -e 's@^%@/@'` IFS="${oIFS}" pathcomp='' while [ $# -ne 0 ] ; do pathcomp="${pathcomp}${1}" shift if [ ! -d "${pathcomp}" ] ; then $mkdirprog "${pathcomp}" else true fi pathcomp="${pathcomp}/" done fi if [ x"$dir_arg" != x ] then $doit $instcmd $dst && if [ x"$chowncmd" != x ]; then $doit $chowncmd $dst; else true ; fi && if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dst; else true ; fi && if [ x"$stripcmd" != x ]; then $doit $stripcmd $dst; else true ; fi && if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dst; else true ; fi else # If we're going to rename the final executable, determine the name now. if [ x"$transformarg" = x ] then dstfile=`basename $dst` else dstfile=`basename $dst $transformbasename | sed $transformarg`$transformbasename fi # don't allow the sed command to completely eliminate the filename if [ x"$dstfile" = x ] then dstfile=`basename $dst` else true fi # Make a temp file name in the proper directory. dsttmp=$dstdir/#inst.$$# # Move or copy the file name to the temp name $doit $instcmd $src $dsttmp && trap "rm -f ${dsttmp}" 0 && # and set any options; do chmod last to preserve setuid bits # If any of these fail, we abort the whole thing. If we want to # ignore errors from any of these, just make sure not to ignore # errors from the above "$doit $instcmd $src $dsttmp" command. if [ x"$chowncmd" != x ]; then $doit $chowncmd $dsttmp; else true;fi && if [ x"$chgrpcmd" != x ]; then $doit $chgrpcmd $dsttmp; else true;fi && if [ x"$stripcmd" != x ]; then $doit $stripcmd $dsttmp; else true;fi && if [ x"$chmodcmd" != x ]; then $doit $chmodcmd $dsttmp; else true;fi && # Now rename the file to the real destination. $doit $rmcmd -f $dstdir/$dstfile && $doit $mvcmd $dsttmp $dstdir/$dstfile fi && exit 0 tsung-1.8.0/Makefile.in0000644000201100017670000002777614377756736014466 0ustar nniclausdream#### CONFIGURE VARIABLE # export ERLC_EMULATOR to fix a bug in R9B with native compilation ERLC_EMULATOR=@ERL@ export ERLC_EMULATOR ERL=@ERL@ ERLC=@ERLC@ SED=@SED@ ERL_OPTS=@ERL_OPTS@ # FIXME DIALYZER=@DIALYZER@ ERLDIR=@ERLANG_ROOT_DIR@ export ERLDIR USENEWTIMEAPI=@erlang_cv_new_time_api@ ERLANG_XMERL_DIR=@ERLANG_LIB_DIR_xmerl@/include raw_erlang_prefix=@libdir@/erlang/ PACKAGE_TARNAME=@PACKAGE_TARNAME@ prefix=@prefix@ exec_prefix=@exec_prefix@ bindir=@bindir@ libdir=@libdir@ datadir=@datadir@ datarootdir=@datarootdir@ docdir=@docdir@ TEMPLATES_SUBDIR=@TEMPLATES_SUBDIR@ CONFIGURE_DEPENDENCIES=@CONFIGURE_DEPENDENCIES@ CONFIG_STATUS_DEPENDENCIES=@CONFIG_STATUS_DEPENDENCIES@ VERSION=@PACKAGE_VERSION@ PACKAGE=@PACKAGE_NAME@ DTD=@DTD@ #### END OF SUBSTITUTION SVN_REVISION=$Revision$ ERL_COMPILER_OPTIONS="[warn_unused_vars]" export ERL_COMPILER_OPTIONS ifeq ($(TYPE),debug) OPT =+debug_info -DDEBUG else ifeq ($(TYPE),native) OPT:=+native else OPT = +strict_record_tests endif endif EBIN = ./ebin EBIN_TEST = ./ebin-test ifeq ($(TYPE),test) OPT =+export_all +debug_info EBIN = $(EBIN_TEST) endif ifeq ($(USENEWTIMEAPI),yes) OPT += -Dnew_time_api endif INC = ./include CC = $(ERLC) ESRC = ./src ifeq ($(TYPE),snapshot) DAY=$(shell date +"%Y%m%d") distdir = $(PACKAGE)-$(VERSION)-$(DAY) else distdir = $(PACKAGE)-$(VERSION) endif # installation path BINDIR = $(bindir) LIBDIR = $(libdir)/tsung/ TOOLS_BINDIR = $(LIBDIR)/bin CONFDIR = $(docdir)/examples SHARE_DIR = $(datadir)/tsung/ TEMPLATES_DIR = $(datadir)/$(TEMPLATES_SUBDIR) MAN_DIR = $(datadir)/man/man1/ DOC_DIR = $(docdir) # custom behaviours BEHAVIORS = $(EBIN)/ts_plugin.beam $(EBIN)/gen_ts_transport.beam ERLANG_LIB_DIR = $(libdir)/erlang/lib APPLICATION = tsung CONTROLLER_APPLICATION = tsung_controller RECORDER_APPLICATION = tsung_recorder RECORDER_TARGETDIR = $(LIBDIR)/$(RECORDER_APPLICATION)-$(VERSION) CONTROLLER_TARGETDIR = $(LIBDIR)/$(CONTROLLER_APPLICATION)-$(VERSION) TARGETDIR = $(LIBDIR)/$(APPLICATION)-$(VERSION) TEMPLATES = $(wildcard $(ESRC)/templates/*.thtml) TEMPLATES_STYLE = $(wildcard $(ESRC)/templates/style/*.js) TEMPLATES_STYLE += $(wildcard $(ESRC)/templates/style/*.css) TMP = $(wildcard *~) $(wildcard src/*~) $(wildcard inc/*~) INC_FILES = $(wildcard $(INC)/*.hrl) LIBSRC = $(wildcard $(ESRC)/lib/*.erl) TESTSRC = $(wildcard $(ESRC)/test/*.erl) SRC = $(wildcard $(ESRC)/$(APPLICATION)/*.erl) CONTROLLER_SRC = $(wildcard $(ESRC)/$(CONTROLLER_APPLICATION)/*.erl) RECORDER_SRC = $(wildcard $(ESRC)/$(RECORDER_APPLICATION)/*.erl) CONFFILE_SRC = $(wildcard examples/*.xml.in) CONFFILE = $(basename $(CONFFILE_SRC)) TEST_CONFFILE_SRC = $(wildcard src/test/*.xml.in) TEST_CONFFILE = $(basename $(TEST_CONFFILE_SRC)) USERMANUAL = docs/_build/latex/Tsung.pdf USERMANUAL_IMG = $(wildcard docs/images/*.png) USERMANUAL_SRC = $(wildcard docs/*.rst docs/*.txt) MANPAGES = $(wildcard man/*.1) PERL_SCRIPTS_SRC = $(wildcard $(ESRC)/*.pl.in) PERL_SCRIPTS = $(basename $(PERL_SCRIPTS_SRC)) TSPLOT_SRC = $(wildcard $(ESRC)/tsung-plotter/*.py.in) TSPLOT = $(basename $(TSPLOT_SRC)) TSUNG_PLOTTER_LIB= $(wildcard $(ESRC)/tsung-plotter/tsung/*.py) TSUNG_PLOTTER_CONF= $(wildcard $(ESRC)/tsung-plotter/tsung/*.conf) $(wildcard $(ESRC)/tsung-plotter/*.conf) TARGET = $(addsuffix .beam, $(basename \ $(addprefix $(EBIN)/, $(notdir $(SRC))))) LIB_TARGET = $(addsuffix .beam, $(basename \ $(addprefix $(EBIN)/, $(notdir $(LIBSRC))))) CONTROLLER_TARGET = $(addsuffix .beam, $(basename \ $(addprefix $(EBIN)/, $(notdir $(CONTROLLER_SRC))))) RECORDER_TARGET = $(addsuffix .beam, $(basename \ $(addprefix $(EBIN)/, $(notdir $(RECORDER_SRC))))) TEST_TARGET = $(addsuffix .beam, $(basename \ $(addprefix $(EBIN)/, $(notdir $(TESTSRC))))) DEBIAN = debian/changelog debian/control debian/compat debian/copyright debian/docs debian/tsung.dirs debian/rules APPFILES = $(EBIN)/$(APPLICATION).app APPFILES_IN = $(ESRC)/$(APPLICATION)/$(APPLICATION).app.in CONTROLLER_APPFILES = $(EBIN)/$(CONTROLLER_APPLICATION).app CONTROLLER_APPFILES_IN = $(ESRC)/$(CONTROLLER_APPLICATION)/$(CONTROLLER_APPLICATION).app.in RECORDER_APPFILES = $(EBIN)/$(RECORDER_APPLICATION).app RECORDER_APPFILES_IN = $(ESRC)/$(RECORDER_APPLICATION)/$(RECORDER_APPLICATION).app.in SCRIPT = $(BINDIR)/tsung REC_SCRIPT = $(BINDIR)/tsung-recorder PWD = $(shell pwd) DIST_COMMON=Makefile.in $(CONFFILE_SRC) $(PERL_SCRIPTS_SRC) $(TSPLOT_SRC) tsung.sh.in tsung-recorder.sh.in tsung.spec.in $(CONTROLLER_APPFILES_IN) DOC_OPTS={def,{version,\"$(VERSION)\"}} .PHONY: doc tsung: Makefile config.status $(PERL_SCRIPTS) $(TSPLOT) tsung.sh tsung-recorder.sh tsung.spec $(TARGET) $(RECORDER_TARGET) $(CONTROLLER_TARGET) $(LIB_TARGET) $(CONTROLLER_APPFILES) $(APPFILES) $(RECORDER_APPFILES) buildtest: $(TEST_TARGET) fulltest: clean test test: @mkdir -p $(EBIN_TEST) $(MAKE) TYPE=test dotest dotest: tsung buildtest $(CONFFILE) $(TEST_CONFFILE) $(ERL) -noshell -pa $(EBIN_TEST) -s ts_test_all run -s init stop edoc: $(ERL) -noshell -eval "edoc:application($(APPLICATION), \"./$(ESRC)/$(APPLICATION)\", [$(DOC_OPTS)])" -s init stop $(ERL) -noshell -eval "edoc:application($(CONTROLLER_APPLICATION), \ \"./$(ESRC)/$(CONTROLLER_APPLICATION)\", [$(DOC_OPTS)])" -s init stop $(ERL) -noshell -eval "edoc:application($(RECORDER_APPLICATION), \ \"./$(ESRC)/$(RECORDER_APPLICATION)\", [$(DOC_OPTS)])" -s init stop # TODO: remove -Wno_behaviours, but only if R15B became a requirement. # see http://erlang.org/pipermail/erlang-questions/2012-January/063608.html dialyzer: $(DIALYZER) -r ebin -I ./include/ -Wno_undefined_callbacks all: clean tsung debug: $(MAKE) TYPE=debug native: $(MAKE) TYPE=native rpm: release tsung.spec rpmbuild -ta $(distdir).tar.gz validate: $(CONFFILE) @for i in $(CONFFILE); do xmlproc_val $$i; done deb: fakeroot debian/rules clean debian/rules build fakeroot debian/rules binary show: @echo $(LIBSRC) clean: -rm -fr $(EBIN_TEST) -rm -f $(TARGET) $(TMP) -rm -f $(RECORDER_APPFILES) $(CONTROLLER_APPFILES) $(APPFILES) -rm -f $(EBIN)/*.app $(PERL_SCRIPTS) $(TSPLOT) $(CONFFILE) -rm -f $(EBIN)/*.beam tsung.sh tsung.spec tsung.xml tsung.sh tsung-recorder.sh -rm -f *.xml config.log src/test/*.xml src/test/usersdb.csv install: tsung doc install_recorder install_controller $(CONFFILE) -rm -f $(TMP) install -d $(DESTDIR)$(TARGETDIR)/priv install -d $(DESTDIR)$(TARGETDIR)/ebin install -d $(DESTDIR)$(TARGETDIR)/src install -d $(DESTDIR)$(TARGETDIR)/include install -d $(DESTDIR)$(TOOLS_BINDIR)/ install -d $(DESTDIR)$(BINDIR)/ install -pm 0644 $(INC_FILES) $(DESTDIR)$(TARGETDIR)/include/ install -pm 0644 $(TARGET) $(DESTDIR)$(TARGETDIR)/ebin/ install -pm 0644 $(LIB_TARGET) $(DESTDIR)$(TARGETDIR)/ebin/ install -pm 0644 $(APPFILES) $(DESTDIR)$(TARGETDIR)/ebin/ install -pm 0644 $(SRC) $(DESTDIR)$(TARGETDIR)/src/ # install the man page install -d $(DESTDIR)$(MAN_DIR) install -pm 0644 $(MANPAGES) $(DESTDIR)$(MAN_DIR) # create startup script install -pm 0755 tsung.sh $(DESTDIR)$(SCRIPT) install -pm 0755 tsung-recorder.sh $(DESTDIR)$(REC_SCRIPT) install -pm 0755 $(PERL_SCRIPTS) $(DESTDIR)$(TOOLS_BINDIR) # tsung-plotter install -pm 0755 $(TSPLOT) $(DESTDIR)$(BINDIR)/tsplot install -d $(DESTDIR)$(LIBDIR)/tsung_plotter install -d $(DESTDIR)$(SHARE_DIR)/tsung_plotter install -pm 0644 $(TSUNG_PLOTTER_LIB) $(DESTDIR)$(LIBDIR)/tsung_plotter install -pm 0644 $(TSUNG_PLOTTER_CONF) $(DESTDIR)$(SHARE_DIR)/tsung_plotter install -d $(DESTDIR)$(CONFDIR) install -pm 0644 $(CONFFILE) $(DESTDIR)$(CONFDIR)/ install -d $(DESTDIR)$(TEMPLATES_DIR) install -d $(DESTDIR)$(TEMPLATES_DIR)/style install -pm 0644 $(TEMPLATES) $(DESTDIR)$(TEMPLATES_DIR)/ install -pm 0644 $(TEMPLATES_STYLE) $(DESTDIR)$(TEMPLATES_DIR)/style install -pm 0644 $(DTD) $(DESTDIR)$(SHARE_DIR)/ install_recorder: install -d $(DESTDIR)$(RECORDER_TARGETDIR)/priv install -d $(DESTDIR)$(RECORDER_TARGETDIR)/ebin install -d $(DESTDIR)$(RECORDER_TARGETDIR)/src install -d $(DESTDIR)$(RECORDER_TARGETDIR)/include install -pm 0644 $(INC_FILES) $(DESTDIR)$(RECORDER_TARGETDIR)/include install -pm 0644 $(RECORDER_TARGET) $(DESTDIR)$(RECORDER_TARGETDIR)/ebin install -pm 0644 $(RECORDER_APPFILES) $(DESTDIR)$(RECORDER_TARGETDIR)/ebin install -pm 0644 $(RECORDER_SRC) $(DESTDIR)$(RECORDER_TARGETDIR)/src install_controller: install -d $(DESTDIR)$(CONTROLLER_TARGETDIR)/priv install -d $(DESTDIR)$(CONTROLLER_TARGETDIR)/ebin install -d $(DESTDIR)$(CONTROLLER_TARGETDIR)/src install -d $(DESTDIR)$(CONTROLLER_TARGETDIR)/include install -pm 0644 $(INC_FILES) $(DESTDIR)$(CONTROLLER_TARGETDIR)/include install -pm 0644 $(CONTROLLER_TARGET) $(DESTDIR)$(CONTROLLER_TARGETDIR)/ebin install -pm 0644 $(CONTROLLER_APPFILES) $(DESTDIR)$(CONTROLLER_TARGETDIR)/ebin install -pm 0644 $(CONTROLLER_SRC) $(DESTDIR)$(CONTROLLER_TARGETDIR)/src uninstall: rm -rf $(TARGETDIR) $(SCRIPT) Makefile: Makefile.in config.status @$(SHELL) ./config.status --file=$@ %.pl: %.pl.in vsn.mk @$(SHELL) ./config.status --file=$@ %.py: %.py.in vsn.mk @$(SHELL) ./config.status --file=$@ %.spec: %.spec.in vsn.mk @$(SHELL) ./config.status --file=$@ %.xml: %.xml.in @$(SHELL) ./config.status --file=$@ %.sh :%.sh.in vsn.mk @$(SHELL) ./config.status --file=$@ $(APPFILES): $(APPFILES_IN) @$(SHELL) ./config.status --file=$@:$< $(CONTROLLER_APPFILES): $(CONTROLLER_APPFILES_IN) @$(SHELL) ./config.status --file=$@:$< $(RECORDER_APPFILES): $(RECORDER_APPFILES_IN) @$(SHELL) ./config.status --file=$@:$< config.status: configure $(CONFIG_STATUS_DEPENDENCIES) $(SHELL) ./config.status --recheck configure: configure.ac $(CONFIGURE_DEPENDENCIES) @echo "running autoconf" @autoconf doc: $(MAKE) -C man man user_guide: $(MAKE) -C docs latexpdf release: Makefile tsung.spec doc user_guide rm -fr $(distdir) mkdir -p $(distdir) tar zcf tmp.tgz $(SRC) $(APPFILES_IN) $(INC_FILES) $(LIBSRC) \ $(CONTROLLER_SRC) $(CONTROLLER_APPFILES_IN) $(TESTSRC) \ $(RECORDER_APPFILES_IN) \ $(RECORDER_SRC) $(TEMPLATES) $(TEMPLATES_STYLE)\ man/*.erl man/*.txt man/*.dia man/*.png man/Makefile man/*.sgml man/*.1 \ docs/*.rst docs/Makefile docs/*.txt docs/README docs/*.py docs/_static docs/_templates \ $(USERMANUAL) $(USERMANUAL_SRC) $(USERMANUAL_IMG) $(DTD) \ COPYING README.md LISEZMOI TODO $(CONFFILE_SRC) $(TEST_CONFFILE_SRC) \ tsung.sh.in vsn.mk src/test/*.csv src/test/*.txt \ src/test/*.out \ $(DEBIAN) $(PERL_SCRIPTS_SRC) CONTRIBUTORS CHANGELOG.md \ $(TSPLOT_SRC) $(TSUNG_PLOTTER_CONF) $(TSUNG_PLOTTER_LIB)\ configure configure.ac config.guess *.m4 config.sub Makefile.in \ install-sh tsung.spec.in tsung.spec tsung-recorder.sh.in tar -C $(distdir) -zxf tmp.tgz mkdir $(distdir)/ebin tar zvcf $(distdir).tar.gz $(distdir) rm -fr $(distdir) rm -fr tmp.tgz snapshot: $(MAKE) TYPE=snapshot release $(EBIN)/%.beam: src/test/%.erl $(INC_FILES) @echo "Compiling test $< ... " @$(CC) -W0 $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -o $(EBIN) $< $(EBIN)/%.beam: src/lib/%.erl $(INC_FILES) @echo "Compiling $< ... " @$(CC) -W0 $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -o $(EBIN) $< # to avoid circular dependency $(EBIN)/ts_plugin.beam: src/$(APPLICATION)/ts_plugin.erl $(INC_FILES) @echo "Compiling $< ... " @$(CC) $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -pa $(EBIN) -o $(EBIN) $< # to avoid circular dependency $(EBIN)/gen_ts_transport.beam: src/$(APPLICATION)/gen_ts_transport.erl $(INC_FILES) @echo "Compiling $< ... " @$(CC) $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -pa $(EBIN) -o $(EBIN) $< $(EBIN)/%.beam: src/$(APPLICATION)/%.erl $(INC_FILES) $(BEHAVIORS) @echo "Compiling $< ... " @$(CC) $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -pa $(EBIN) -o $(EBIN) $< $(EBIN)/%.beam: src/$(RECORDER_APPLICATION)/%.erl $(INC_FILES) $(BEHAVIORS) @echo "Compiling $< ... " @$(CC) $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -pa $(EBIN) -o $(EBIN) $< $(EBIN)/%.beam: src/$(CONTROLLER_APPLICATION)/%.erl $(INC_FILES) $(BEHAVIORS) @echo "Compiling $< ... " @$(CC) $(OPT) -I $(INC) -I $(ERLANG_XMERL_DIR) -pa $(EBIN) -o $(EBIN) $< %:%.sh # Override makefile default implicit rule tsung-1.8.0/config.sub0000644000201100017670000007511314377756736014365 0ustar nniclausdream#! /bin/sh # Configuration validation subroutine script. # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, # 2000, 2001, 2002, 2003, 2004 Free Software Foundation, Inc. timestamp='2004-08-29' # This file is (in principle) common to ALL GNU software. # The presence of a machine in this file suggests that SOME GNU software # can handle that machine. It does not imply ALL GNU software can. # # This file is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA. # As a special exception to the GNU General Public License, if you # distribute this file as part of a program that contains a # configuration script generated by Autoconf, you may include it under # the same distribution terms that you use for the rest of that program. # Please send patches to . Submit a context # diff and a properly formatted ChangeLog entry. # # Configuration subroutine to validate and canonicalize a configuration type. # Supply the specified configuration type as an argument. # If it is invalid, we print an error message on stderr and exit with code 1. # Otherwise, we print the canonical config type on stdout and succeed. # This file is supposed to be the same for all GNU packages # and recognize all the CPU types, system types and aliases # that are meaningful with *any* GNU software. # Each package is responsible for reporting which valid configurations # it does not support. The user should be able to distinguish # a failure to support a valid configuration from a meaningless # configuration. # The goal of this file is to map all the various variations of a given # machine specification into a single specification in the form: # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM # or in some cases, the newer four-part form: # CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM # It is wrong to echo any other type of specification. me=`echo "$0" | sed -e 's,.*/,,'` usage="\ Usage: $0 [OPTION] CPU-MFR-OPSYS $0 [OPTION] ALIAS Canonicalize a configuration name. Operation modes: -h, --help print this help, then exit -t, --time-stamp print date of last modification, then exit -v, --version print version number, then exit Report bugs and patches to ." version="\ GNU config.sub ($timestamp) Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." help=" Try \`$me --help' for more information." # Parse command line while test $# -gt 0 ; do case $1 in --time-stamp | --time* | -t ) echo "$timestamp" ; exit 0 ;; --version | -v ) echo "$version" ; exit 0 ;; --help | --h* | -h ) echo "$usage"; exit 0 ;; -- ) # Stop option processing shift; break ;; - ) # Use stdin as input. break ;; -* ) echo "$me: invalid option $1$help" exit 1 ;; *local*) # First pass through any local machine types. echo $1 exit 0;; * ) break ;; esac done case $# in 0) echo "$me: missing argument$help" >&2 exit 1;; 1) ;; *) echo "$me: too many arguments$help" >&2 exit 1;; esac # Separate what the user gave into CPU-COMPANY and OS or KERNEL-OS (if any). # Here we must recognize all the valid KERNEL-OS combinations. maybe_os=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\2/'` case $maybe_os in nto-qnx* | linux-gnu* | linux-dietlibc | linux-uclibc* | uclinux-uclibc* | uclinux-gnu* | \ kfreebsd*-gnu* | knetbsd*-gnu* | netbsd*-gnu* | storm-chaos* | os2-emx* | rtmk-nova*) os=-$maybe_os basic_machine=`echo $1 | sed 's/^\(.*\)-\([^-]*-[^-]*\)$/\1/'` ;; *) basic_machine=`echo $1 | sed 's/-[^-]*$//'` if [ $basic_machine != $1 ] then os=`echo $1 | sed 's/.*-/-/'` else os=; fi ;; esac ### Let's recognize common machines as not being operating systems so ### that things like config.sub decstation-3100 work. We also ### recognize some manufacturers as not being operating systems, so we ### can provide default operating systems below. case $os in -sun*os*) # Prevent following clause from handling this invalid input. ;; -dec* | -mips* | -sequent* | -encore* | -pc532* | -sgi* | -sony* | \ -att* | -7300* | -3300* | -delta* | -motorola* | -sun[234]* | \ -unicom* | -ibm* | -next | -hp | -isi* | -apollo | -altos* | \ -convergent* | -ncr* | -news | -32* | -3600* | -3100* | -hitachi* |\ -c[123]* | -convex* | -sun | -crds | -omron* | -dg | -ultra | -tti* | \ -harris | -dolphin | -highlevel | -gould | -cbm | -ns | -masscomp | \ -apple | -axis | -knuth | -cray) os= basic_machine=$1 ;; -sim | -cisco | -oki | -wec | -winbond) os= basic_machine=$1 ;; -scout) ;; -wrs) os=-vxworks basic_machine=$1 ;; -chorusos*) os=-chorusos basic_machine=$1 ;; -chorusrdb) os=-chorusrdb basic_machine=$1 ;; -hiux*) os=-hiuxwe2 ;; -sco5) os=-sco3.2v5 basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -sco4) os=-sco3.2v4 basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -sco3.2.[4-9]*) os=`echo $os | sed -e 's/sco3.2./sco3.2v/'` basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -sco3.2v[4-9]*) # Don't forget version if it is 3.2v4 or newer. basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -sco*) os=-sco3.2v2 basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -udk*) basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -isc) os=-isc2.2 basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -clix*) basic_machine=clipper-intergraph ;; -isc*) basic_machine=`echo $1 | sed -e 's/86-.*/86-pc/'` ;; -lynx*) os=-lynxos ;; -ptx*) basic_machine=`echo $1 | sed -e 's/86-.*/86-sequent/'` ;; -windowsnt*) os=`echo $os | sed -e 's/windowsnt/winnt/'` ;; -psos*) os=-psos ;; -mint | -mint[0-9]*) basic_machine=m68k-atari os=-mint ;; esac # Decode aliases for certain CPU-COMPANY combinations. case $basic_machine in # Recognize the basic CPU types without company name. # Some are omitted here because they have special meanings below. 1750a | 580 \ | a29k \ | alpha | alphaev[4-8] | alphaev56 | alphaev6[78] | alphapca5[67] \ | alpha64 | alpha64ev[4-8] | alpha64ev56 | alpha64ev6[78] | alpha64pca5[67] \ | am33_2.0 \ | arc | arm | arm[bl]e | arme[lb] | armv[2345] | armv[345][lb] | avr \ | c4x | clipper \ | d10v | d30v | dlx | dsp16xx \ | fr30 | frv \ | h8300 | h8500 | hppa | hppa1.[01] | hppa2.0 | hppa2.0[nw] | hppa64 \ | i370 | i860 | i960 | ia64 \ | ip2k | iq2000 \ | m32r | m32rle | m68000 | m68k | m88k | mcore \ | mips | mipsbe | mipseb | mipsel | mipsle \ | mips16 \ | mips64 | mips64el \ | mips64vr | mips64vrel \ | mips64orion | mips64orionel \ | mips64vr4100 | mips64vr4100el \ | mips64vr4300 | mips64vr4300el \ | mips64vr5000 | mips64vr5000el \ | mipsisa32 | mipsisa32el \ | mipsisa32r2 | mipsisa32r2el \ | mipsisa64 | mipsisa64el \ | mipsisa64r2 | mipsisa64r2el \ | mipsisa64sb1 | mipsisa64sb1el \ | mipsisa64sr71k | mipsisa64sr71kel \ | mipstx39 | mipstx39el \ | mn10200 | mn10300 \ | msp430 \ | ns16k | ns32k \ | openrisc | or32 \ | pdp10 | pdp11 | pj | pjl \ | powerpc | powerpc64 | powerpc64le | powerpcle | ppcbe \ | pyramid \ | sh | sh[1234] | sh[23]e | sh[34]eb | shbe | shle | sh[1234]le | sh3ele \ | sh64 | sh64le \ | sparc | sparc64 | sparc86x | sparclet | sparclite | sparcv8 | sparcv9 | sparcv9b \ | strongarm \ | tahoe | thumb | tic4x | tic80 | tron \ | v850 | v850e \ | we32k \ | x86 | xscale | xstormy16 | xtensa \ | z8k) basic_machine=$basic_machine-unknown ;; m6811 | m68hc11 | m6812 | m68hc12) # Motorola 68HC11/12. basic_machine=$basic_machine-unknown os=-none ;; m88110 | m680[12346]0 | m683?2 | m68360 | m5200 | v70 | w65 | z8k) ;; # We use `pc' rather than `unknown' # because (1) that's what they normally are, and # (2) the word "unknown" tends to confuse beginning users. i*86 | x86_64) basic_machine=$basic_machine-pc ;; # Object if more than one company name word. *-*-*) echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2 exit 1 ;; # Recognize the basic CPU types with company name. 580-* \ | a29k-* \ | alpha-* | alphaev[4-8]-* | alphaev56-* | alphaev6[78]-* \ | alpha64-* | alpha64ev[4-8]-* | alpha64ev56-* | alpha64ev6[78]-* \ | alphapca5[67]-* | alpha64pca5[67]-* | arc-* \ | arm-* | armbe-* | armle-* | armeb-* | armv*-* \ | avr-* \ | bs2000-* \ | c[123]* | c30-* | [cjt]90-* | c4x-* | c54x-* | c55x-* | c6x-* \ | clipper-* | craynv-* | cydra-* \ | d10v-* | d30v-* | dlx-* \ | elxsi-* \ | f30[01]-* | f700-* | fr30-* | frv-* | fx80-* \ | h8300-* | h8500-* \ | hppa-* | hppa1.[01]-* | hppa2.0-* | hppa2.0[nw]-* | hppa64-* \ | i*86-* | i860-* | i960-* | ia64-* \ | ip2k-* | iq2000-* \ | m32r-* | m32rle-* \ | m68000-* | m680[012346]0-* | m68360-* | m683?2-* | m68k-* \ | m88110-* | m88k-* | mcore-* \ | mips-* | mipsbe-* | mipseb-* | mipsel-* | mipsle-* \ | mips16-* \ | mips64-* | mips64el-* \ | mips64vr-* | mips64vrel-* \ | mips64orion-* | mips64orionel-* \ | mips64vr4100-* | mips64vr4100el-* \ | mips64vr4300-* | mips64vr4300el-* \ | mips64vr5000-* | mips64vr5000el-* \ | mipsisa32-* | mipsisa32el-* \ | mipsisa32r2-* | mipsisa32r2el-* \ | mipsisa64-* | mipsisa64el-* \ | mipsisa64r2-* | mipsisa64r2el-* \ | mipsisa64sb1-* | mipsisa64sb1el-* \ | mipsisa64sr71k-* | mipsisa64sr71kel-* \ | mipstx39-* | mipstx39el-* \ | mmix-* \ | msp430-* \ | none-* | np1-* | ns16k-* | ns32k-* \ | orion-* \ | pdp10-* | pdp11-* | pj-* | pjl-* | pn-* | power-* \ | powerpc-* | powerpc64-* | powerpc64le-* | powerpcle-* | ppcbe-* \ | pyramid-* \ | romp-* | rs6000-* \ | sh-* | sh[1234]-* | sh[23]e-* | sh[34]eb-* | shbe-* \ | shle-* | sh[1234]le-* | sh3ele-* | sh64-* | sh64le-* \ | sparc-* | sparc64-* | sparc86x-* | sparclet-* | sparclite-* \ | sparcv8-* | sparcv9-* | sparcv9b-* | strongarm-* | sv1-* | sx?-* \ | tahoe-* | thumb-* \ | tic30-* | tic4x-* | tic54x-* | tic55x-* | tic6x-* | tic80-* \ | tron-* \ | v850-* | v850e-* | vax-* \ | we32k-* \ | x86-* | x86_64-* | xps100-* | xscale-* | xstormy16-* \ | xtensa-* \ | ymp-* \ | z8k-*) ;; # Recognize the various machine names and aliases which stand # for a CPU type and a company and sometimes even an OS. 386bsd) basic_machine=i386-unknown os=-bsd ;; 3b1 | 7300 | 7300-att | att-7300 | pc7300 | safari | unixpc) basic_machine=m68000-att ;; 3b*) basic_machine=we32k-att ;; a29khif) basic_machine=a29k-amd os=-udi ;; abacus) basic_machine=abacus-unknown ;; adobe68k) basic_machine=m68010-adobe os=-scout ;; alliant | fx80) basic_machine=fx80-alliant ;; altos | altos3068) basic_machine=m68k-altos ;; am29k) basic_machine=a29k-none os=-bsd ;; amd64) basic_machine=x86_64-pc ;; amd64-*) basic_machine=x86_64-`echo $basic_machine | sed 's/^[^-]*-//'` ;; amdahl) basic_machine=580-amdahl os=-sysv ;; amiga | amiga-*) basic_machine=m68k-unknown ;; amigaos | amigados) basic_machine=m68k-unknown os=-amigaos ;; amigaunix | amix) basic_machine=m68k-unknown os=-sysv4 ;; apollo68) basic_machine=m68k-apollo os=-sysv ;; apollo68bsd) basic_machine=m68k-apollo os=-bsd ;; aux) basic_machine=m68k-apple os=-aux ;; balance) basic_machine=ns32k-sequent os=-dynix ;; c90) basic_machine=c90-cray os=-unicos ;; convex-c1) basic_machine=c1-convex os=-bsd ;; convex-c2) basic_machine=c2-convex os=-bsd ;; convex-c32) basic_machine=c32-convex os=-bsd ;; convex-c34) basic_machine=c34-convex os=-bsd ;; convex-c38) basic_machine=c38-convex os=-bsd ;; cray | j90) basic_machine=j90-cray os=-unicos ;; craynv) basic_machine=craynv-cray os=-unicosmp ;; cr16c) basic_machine=cr16c-unknown os=-elf ;; crds | unos) basic_machine=m68k-crds ;; crisv32 | crisv32-* | etraxfs*) basic_machine=crisv32-axis ;; cris | cris-* | etrax*) basic_machine=cris-axis ;; crx) basic_machine=crx-unknown os=-elf ;; da30 | da30-*) basic_machine=m68k-da30 ;; decstation | decstation-3100 | pmax | pmax-* | pmin | dec3100 | decstatn) basic_machine=mips-dec ;; decsystem10* | dec10*) basic_machine=pdp10-dec os=-tops10 ;; decsystem20* | dec20*) basic_machine=pdp10-dec os=-tops20 ;; delta | 3300 | motorola-3300 | motorola-delta \ | 3300-motorola | delta-motorola) basic_machine=m68k-motorola ;; delta88) basic_machine=m88k-motorola os=-sysv3 ;; dpx20 | dpx20-*) basic_machine=rs6000-bull os=-bosx ;; dpx2* | dpx2*-bull) basic_machine=m68k-bull os=-sysv3 ;; ebmon29k) basic_machine=a29k-amd os=-ebmon ;; elxsi) basic_machine=elxsi-elxsi os=-bsd ;; encore | umax | mmax) basic_machine=ns32k-encore ;; es1800 | OSE68k | ose68k | ose | OSE) basic_machine=m68k-ericsson os=-ose ;; fx2800) basic_machine=i860-alliant ;; genix) basic_machine=ns32k-ns ;; gmicro) basic_machine=tron-gmicro os=-sysv ;; go32) basic_machine=i386-pc os=-go32 ;; h3050r* | hiux*) basic_machine=hppa1.1-hitachi os=-hiuxwe2 ;; h8300hms) basic_machine=h8300-hitachi os=-hms ;; h8300xray) basic_machine=h8300-hitachi os=-xray ;; h8500hms) basic_machine=h8500-hitachi os=-hms ;; harris) basic_machine=m88k-harris os=-sysv3 ;; hp300-*) basic_machine=m68k-hp ;; hp300bsd) basic_machine=m68k-hp os=-bsd ;; hp300hpux) basic_machine=m68k-hp os=-hpux ;; hp3k9[0-9][0-9] | hp9[0-9][0-9]) basic_machine=hppa1.0-hp ;; hp9k2[0-9][0-9] | hp9k31[0-9]) basic_machine=m68000-hp ;; hp9k3[2-9][0-9]) basic_machine=m68k-hp ;; hp9k6[0-9][0-9] | hp6[0-9][0-9]) basic_machine=hppa1.0-hp ;; hp9k7[0-79][0-9] | hp7[0-79][0-9]) basic_machine=hppa1.1-hp ;; hp9k78[0-9] | hp78[0-9]) # FIXME: really hppa2.0-hp basic_machine=hppa1.1-hp ;; hp9k8[67]1 | hp8[67]1 | hp9k80[24] | hp80[24] | hp9k8[78]9 | hp8[78]9 | hp9k893 | hp893) # FIXME: really hppa2.0-hp basic_machine=hppa1.1-hp ;; hp9k8[0-9][13679] | hp8[0-9][13679]) basic_machine=hppa1.1-hp ;; hp9k8[0-9][0-9] | hp8[0-9][0-9]) basic_machine=hppa1.0-hp ;; hppa-next) os=-nextstep3 ;; hppaosf) basic_machine=hppa1.1-hp os=-osf ;; hppro) basic_machine=hppa1.1-hp os=-proelf ;; i370-ibm* | ibm*) basic_machine=i370-ibm ;; # I'm not sure what "Sysv32" means. Should this be sysv3.2? i*86v32) basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` os=-sysv32 ;; i*86v4*) basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` os=-sysv4 ;; i*86v) basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` os=-sysv ;; i*86sol2) basic_machine=`echo $1 | sed -e 's/86.*/86-pc/'` os=-solaris2 ;; i386mach) basic_machine=i386-mach os=-mach ;; i386-vsta | vsta) basic_machine=i386-unknown os=-vsta ;; iris | iris4d) basic_machine=mips-sgi case $os in -irix*) ;; *) os=-irix4 ;; esac ;; isi68 | isi) basic_machine=m68k-isi os=-sysv ;; m88k-omron*) basic_machine=m88k-omron ;; magnum | m3230) basic_machine=mips-mips os=-sysv ;; merlin) basic_machine=ns32k-utek os=-sysv ;; mingw32) basic_machine=i386-pc os=-mingw32 ;; miniframe) basic_machine=m68000-convergent ;; *mint | -mint[0-9]* | *MiNT | *MiNT[0-9]*) basic_machine=m68k-atari os=-mint ;; mips3*-*) basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'` ;; mips3*) basic_machine=`echo $basic_machine | sed -e 's/mips3/mips64/'`-unknown ;; monitor) basic_machine=m68k-rom68k os=-coff ;; morphos) basic_machine=powerpc-unknown os=-morphos ;; msdos) basic_machine=i386-pc os=-msdos ;; mvs) basic_machine=i370-ibm os=-mvs ;; ncr3000) basic_machine=i486-ncr os=-sysv4 ;; netbsd386) basic_machine=i386-unknown os=-netbsd ;; netwinder) basic_machine=armv4l-rebel os=-linux ;; news | news700 | news800 | news900) basic_machine=m68k-sony os=-newsos ;; news1000) basic_machine=m68030-sony os=-newsos ;; news-3600 | risc-news) basic_machine=mips-sony os=-newsos ;; necv70) basic_machine=v70-nec os=-sysv ;; next | m*-next ) basic_machine=m68k-next case $os in -nextstep* ) ;; -ns2*) os=-nextstep2 ;; *) os=-nextstep3 ;; esac ;; nh3000) basic_machine=m68k-harris os=-cxux ;; nh[45]000) basic_machine=m88k-harris os=-cxux ;; nindy960) basic_machine=i960-intel os=-nindy ;; mon960) basic_machine=i960-intel os=-mon960 ;; nonstopux) basic_machine=mips-compaq os=-nonstopux ;; np1) basic_machine=np1-gould ;; nsr-tandem) basic_machine=nsr-tandem ;; op50n-* | op60c-*) basic_machine=hppa1.1-oki os=-proelf ;; or32 | or32-*) basic_machine=or32-unknown os=-coff ;; os400) basic_machine=powerpc-ibm os=-os400 ;; OSE68000 | ose68000) basic_machine=m68000-ericsson os=-ose ;; os68k) basic_machine=m68k-none os=-os68k ;; pa-hitachi) basic_machine=hppa1.1-hitachi os=-hiuxwe2 ;; paragon) basic_machine=i860-intel os=-osf ;; pbd) basic_machine=sparc-tti ;; pbb) basic_machine=m68k-tti ;; pc532 | pc532-*) basic_machine=ns32k-pc532 ;; pentium | p5 | k5 | k6 | nexgen | viac3) basic_machine=i586-pc ;; pentiumpro | p6 | 6x86 | athlon | athlon_*) basic_machine=i686-pc ;; pentiumii | pentium2 | pentiumiii | pentium3) basic_machine=i686-pc ;; pentium4) basic_machine=i786-pc ;; pentium-* | p5-* | k5-* | k6-* | nexgen-* | viac3-*) basic_machine=i586-`echo $basic_machine | sed 's/^[^-]*-//'` ;; pentiumpro-* | p6-* | 6x86-* | athlon-*) basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'` ;; pentiumii-* | pentium2-* | pentiumiii-* | pentium3-*) basic_machine=i686-`echo $basic_machine | sed 's/^[^-]*-//'` ;; pentium4-*) basic_machine=i786-`echo $basic_machine | sed 's/^[^-]*-//'` ;; pn) basic_machine=pn-gould ;; power) basic_machine=power-ibm ;; ppc) basic_machine=powerpc-unknown ;; ppc-*) basic_machine=powerpc-`echo $basic_machine | sed 's/^[^-]*-//'` ;; ppcle | powerpclittle | ppc-le | powerpc-little) basic_machine=powerpcle-unknown ;; ppcle-* | powerpclittle-*) basic_machine=powerpcle-`echo $basic_machine | sed 's/^[^-]*-//'` ;; ppc64) basic_machine=powerpc64-unknown ;; ppc64-*) basic_machine=powerpc64-`echo $basic_machine | sed 's/^[^-]*-//'` ;; ppc64le | powerpc64little | ppc64-le | powerpc64-little) basic_machine=powerpc64le-unknown ;; ppc64le-* | powerpc64little-*) basic_machine=powerpc64le-`echo $basic_machine | sed 's/^[^-]*-//'` ;; ps2) basic_machine=i386-ibm ;; pw32) basic_machine=i586-unknown os=-pw32 ;; rom68k) basic_machine=m68k-rom68k os=-coff ;; rm[46]00) basic_machine=mips-siemens ;; rtpc | rtpc-*) basic_machine=romp-ibm ;; s390 | s390-*) basic_machine=s390-ibm ;; s390x | s390x-*) basic_machine=s390x-ibm ;; sa29200) basic_machine=a29k-amd os=-udi ;; sb1) basic_machine=mipsisa64sb1-unknown ;; sb1el) basic_machine=mipsisa64sb1el-unknown ;; sei) basic_machine=mips-sei os=-seiux ;; sequent) basic_machine=i386-sequent ;; sh) basic_machine=sh-hitachi os=-hms ;; sh64) basic_machine=sh64-unknown ;; sparclite-wrs | simso-wrs) basic_machine=sparclite-wrs os=-vxworks ;; sps7) basic_machine=m68k-bull os=-sysv2 ;; spur) basic_machine=spur-unknown ;; st2000) basic_machine=m68k-tandem ;; stratus) basic_machine=i860-stratus os=-sysv4 ;; sun2) basic_machine=m68000-sun ;; sun2os3) basic_machine=m68000-sun os=-sunos3 ;; sun2os4) basic_machine=m68000-sun os=-sunos4 ;; sun3os3) basic_machine=m68k-sun os=-sunos3 ;; sun3os4) basic_machine=m68k-sun os=-sunos4 ;; sun4os3) basic_machine=sparc-sun os=-sunos3 ;; sun4os4) basic_machine=sparc-sun os=-sunos4 ;; sun4sol2) basic_machine=sparc-sun os=-solaris2 ;; sun3 | sun3-*) basic_machine=m68k-sun ;; sun4) basic_machine=sparc-sun ;; sun386 | sun386i | roadrunner) basic_machine=i386-sun ;; sv1) basic_machine=sv1-cray os=-unicos ;; symmetry) basic_machine=i386-sequent os=-dynix ;; t3e) basic_machine=alphaev5-cray os=-unicos ;; t90) basic_machine=t90-cray os=-unicos ;; tic54x | c54x*) basic_machine=tic54x-unknown os=-coff ;; tic55x | c55x*) basic_machine=tic55x-unknown os=-coff ;; tic6x | c6x*) basic_machine=tic6x-unknown os=-coff ;; tx39) basic_machine=mipstx39-unknown ;; tx39el) basic_machine=mipstx39el-unknown ;; toad1) basic_machine=pdp10-xkl os=-tops20 ;; tower | tower-32) basic_machine=m68k-ncr ;; tpf) basic_machine=s390x-ibm os=-tpf ;; udi29k) basic_machine=a29k-amd os=-udi ;; ultra3) basic_machine=a29k-nyu os=-sym1 ;; v810 | necv810) basic_machine=v810-nec os=-none ;; vaxv) basic_machine=vax-dec os=-sysv ;; vms) basic_machine=vax-dec os=-vms ;; vpp*|vx|vx-*) basic_machine=f301-fujitsu ;; vxworks960) basic_machine=i960-wrs os=-vxworks ;; vxworks68) basic_machine=m68k-wrs os=-vxworks ;; vxworks29k) basic_machine=a29k-wrs os=-vxworks ;; w65*) basic_machine=w65-wdc os=-none ;; w89k-*) basic_machine=hppa1.1-winbond os=-proelf ;; xps | xps100) basic_machine=xps100-honeywell ;; ymp) basic_machine=ymp-cray os=-unicos ;; z8k-*-coff) basic_machine=z8k-unknown os=-sim ;; none) basic_machine=none-none os=-none ;; # Here we handle the default manufacturer of certain CPU types. It is in # some cases the only manufacturer, in others, it is the most popular. w89k) basic_machine=hppa1.1-winbond ;; op50n) basic_machine=hppa1.1-oki ;; op60c) basic_machine=hppa1.1-oki ;; romp) basic_machine=romp-ibm ;; mmix) basic_machine=mmix-knuth ;; rs6000) basic_machine=rs6000-ibm ;; vax) basic_machine=vax-dec ;; pdp10) # there are many clones, so DEC is not a safe bet basic_machine=pdp10-unknown ;; pdp11) basic_machine=pdp11-dec ;; we32k) basic_machine=we32k-att ;; sh3 | sh4 | sh[34]eb | sh[1234]le | sh[23]ele) basic_machine=sh-unknown ;; sh64) basic_machine=sh64-unknown ;; sparc | sparcv8 | sparcv9 | sparcv9b) basic_machine=sparc-sun ;; cydra) basic_machine=cydra-cydrome ;; orion) basic_machine=orion-highlevel ;; orion105) basic_machine=clipper-highlevel ;; mac | mpw | mac-mpw) basic_machine=m68k-apple ;; pmac | pmac-mpw) basic_machine=powerpc-apple ;; *-unknown) # Make sure to match an already-canonicalized machine name. ;; *) echo Invalid configuration \`$1\': machine \`$basic_machine\' not recognized 1>&2 exit 1 ;; esac # Here we canonicalize certain aliases for manufacturers. case $basic_machine in *-digital*) basic_machine=`echo $basic_machine | sed 's/digital.*/dec/'` ;; *-commodore*) basic_machine=`echo $basic_machine | sed 's/commodore.*/cbm/'` ;; *) ;; esac # Decode manufacturer-specific aliases for certain operating systems. if [ x"$os" != x"" ] then case $os in # First match some system type aliases # that might get confused with valid system types. # -solaris* is a basic system type, with this one exception. -solaris1 | -solaris1.*) os=`echo $os | sed -e 's|solaris1|sunos4|'` ;; -solaris) os=-solaris2 ;; -svr4*) os=-sysv4 ;; -unixware*) os=-sysv4.2uw ;; -gnu/linux*) os=`echo $os | sed -e 's|gnu/linux|linux-gnu|'` ;; # First accept the basic system types. # The portable systems comes first. # Each alternative MUST END IN A *, to match a version number. # -sysv* is not here because it comes later, after sysvr4. -gnu* | -bsd* | -mach* | -minix* | -genix* | -ultrix* | -irix* \ | -*vms* | -sco* | -esix* | -isc* | -aix* | -sunos | -sunos[34]*\ | -hpux* | -unos* | -osf* | -luna* | -dgux* | -solaris* | -sym* \ | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \ | -aos* \ | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \ | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \ | -hiux* | -386bsd* | -knetbsd* | -mirbsd* | -netbsd* | -openbsd* \ | -ekkobsd* | -kfreebsd* | -freebsd* | -riscix* | -lynxos* \ | -bosx* | -nextstep* | -cxux* | -aout* | -elf* | -oabi* \ | -ptx* | -coff* | -ecoff* | -winnt* | -domain* | -vsta* \ | -udi* | -eabi* | -lites* | -ieee* | -go32* | -aux* \ | -chorusos* | -chorusrdb* \ | -cygwin* | -pe* | -psos* | -moss* | -proelf* | -rtems* \ | -mingw32* | -linux-gnu* | -linux-uclibc* | -uxpv* | -beos* | -mpeix* | -udk* \ | -interix* | -uwin* | -mks* | -rhapsody* | -darwin* | -opened* \ | -openstep* | -oskit* | -conix* | -pw32* | -nonstopux* \ | -storm-chaos* | -tops10* | -tenex* | -tops20* | -its* \ | -os2* | -vos* | -palmos* | -uclinux* | -nucleus* \ | -morphos* | -superux* | -rtmk* | -rtmk-nova* | -windiss* \ | -powermax* | -dnix* | -nx6 | -nx7 | -sei* | -dragonfly*) # Remember, each alternative MUST END IN *, to match a version number. ;; -qnx*) case $basic_machine in x86-* | i*86-*) ;; *) os=-nto$os ;; esac ;; -nto-qnx*) ;; -nto*) os=`echo $os | sed -e 's|nto|nto-qnx|'` ;; -sim | -es1800* | -hms* | -xray | -os68k* | -none* | -v88r* \ | -windows* | -osx | -abug | -netware* | -os9* | -beos* \ | -macos* | -mpw* | -magic* | -mmixware* | -mon960* | -lnews*) ;; -mac*) os=`echo $os | sed -e 's|mac|macos|'` ;; -linux-dietlibc) os=-linux-dietlibc ;; -linux*) os=`echo $os | sed -e 's|linux|linux-gnu|'` ;; -sunos5*) os=`echo $os | sed -e 's|sunos5|solaris2|'` ;; -sunos6*) os=`echo $os | sed -e 's|sunos6|solaris3|'` ;; -opened*) os=-openedition ;; -os400*) os=-os400 ;; -wince*) os=-wince ;; -osfrose*) os=-osfrose ;; -osf*) os=-osf ;; -utek*) os=-bsd ;; -dynix*) os=-bsd ;; -acis*) os=-aos ;; -atheos*) os=-atheos ;; -syllable*) os=-syllable ;; -386bsd) os=-bsd ;; -ctix* | -uts*) os=-sysv ;; -nova*) os=-rtmk-nova ;; -ns2 ) os=-nextstep2 ;; -nsk*) os=-nsk ;; # Preserve the version number of sinix5. -sinix5.*) os=`echo $os | sed -e 's|sinix|sysv|'` ;; -sinix*) os=-sysv4 ;; -tpf*) os=-tpf ;; -triton*) os=-sysv3 ;; -oss*) os=-sysv3 ;; -svr4) os=-sysv4 ;; -svr3) os=-sysv3 ;; -sysvr4) os=-sysv4 ;; # This must come after -sysvr4. -sysv*) ;; -ose*) os=-ose ;; -es1800*) os=-ose ;; -xenix) os=-xenix ;; -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*) os=-mint ;; -aros*) os=-aros ;; -kaos*) os=-kaos ;; -none) ;; *) # Get rid of the `-' at the beginning of $os. os=`echo $os | sed 's/[^-]*-//'` echo Invalid configuration \`$1\': system \`$os\' not recognized 1>&2 exit 1 ;; esac else # Here we handle the default operating systems that come with various machines. # The value should be what the vendor currently ships out the door with their # machine or put another way, the most popular os provided with the machine. # Note that if you're going to try to match "-MANUFACTURER" here (say, # "-sun"), then you have to tell the case statement up towards the top # that MANUFACTURER isn't an operating system. Otherwise, code above # will signal an error saying that MANUFACTURER isn't an operating # system, and we'll never get to this point. case $basic_machine in *-acorn) os=-riscix1.2 ;; arm*-rebel) os=-linux ;; arm*-semi) os=-aout ;; c4x-* | tic4x-*) os=-coff ;; # This must come before the *-dec entry. pdp10-*) os=-tops20 ;; pdp11-*) os=-none ;; *-dec | vax-*) os=-ultrix4.2 ;; m68*-apollo) os=-domain ;; i386-sun) os=-sunos4.0.2 ;; m68000-sun) os=-sunos3 # This also exists in the configure program, but was not the # default. # os=-sunos4 ;; m68*-cisco) os=-aout ;; mips*-cisco) os=-elf ;; mips*-*) os=-elf ;; or32-*) os=-coff ;; *-tti) # must be before sparc entry or we get the wrong os. os=-sysv3 ;; sparc-* | *-sun) os=-sunos4.1.1 ;; *-be) os=-beos ;; *-ibm) os=-aix ;; *-knuth) os=-mmixware ;; *-wec) os=-proelf ;; *-winbond) os=-proelf ;; *-oki) os=-proelf ;; *-hp) os=-hpux ;; *-hitachi) os=-hiux ;; i860-* | *-att | *-ncr | *-altos | *-motorola | *-convergent) os=-sysv ;; *-cbm) os=-amigaos ;; *-dg) os=-dgux ;; *-dolphin) os=-sysv3 ;; m68k-ccur) os=-rtu ;; m88k-omron*) os=-luna ;; *-next ) os=-nextstep ;; *-sequent) os=-ptx ;; *-crds) os=-unos ;; *-ns) os=-genix ;; i370-*) os=-mvs ;; *-next) os=-nextstep3 ;; *-gould) os=-sysv ;; *-highlevel) os=-bsd ;; *-encore) os=-bsd ;; *-sgi) os=-irix ;; *-siemens) os=-sysv4 ;; *-masscomp) os=-rtu ;; f30[01]-fujitsu | f700-fujitsu) os=-uxpv ;; *-rom68k) os=-coff ;; *-*bug) os=-coff ;; *-apple) os=-macos ;; *-atari*) os=-mint ;; *) os=-none ;; esac fi # Here we handle the case where we know the os, and the CPU type, but not the # manufacturer. We pick the logical manufacturer. vendor=unknown case $basic_machine in *-unknown) case $os in -riscix*) vendor=acorn ;; -sunos*) vendor=sun ;; -aix*) vendor=ibm ;; -beos*) vendor=be ;; -hpux*) vendor=hp ;; -mpeix*) vendor=hp ;; -hiux*) vendor=hitachi ;; -unos*) vendor=crds ;; -dgux*) vendor=dg ;; -luna*) vendor=omron ;; -genix*) vendor=ns ;; -mvs* | -opened*) vendor=ibm ;; -os400*) vendor=ibm ;; -ptx*) vendor=sequent ;; -tpf*) vendor=ibm ;; -vxsim* | -vxworks* | -windiss*) vendor=wrs ;; -aux*) vendor=apple ;; -hms*) vendor=hitachi ;; -mpw* | -macos*) vendor=apple ;; -*mint | -mint[0-9]* | -*MiNT | -MiNT[0-9]*) vendor=atari ;; -vos*) vendor=stratus ;; esac basic_machine=`echo $basic_machine | sed "s/unknown/$vendor/"` ;; esac echo $basic_machine$os exit 0 # Local variables: # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "timestamp='" # time-stamp-format: "%:y-%02m-%02d" # time-stamp-end: "'" # End: tsung-1.8.0/aclocal.m40000644000201100017670000000114414377756736014236 0ustar nniclausdream# generated automatically by aclocal 1.10 -*- Autoconf -*- # Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, # 2005, 2006 Free Software Foundation, Inc. # This file is free software; the Free Software Foundation # gives unlimited permission to copy and/or distribute it, # with or without modifications, as long as this notice is preserved. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY, to the extent permitted by law; without # even the implied warranty of MERCHANTABILITY or FITNESS FOR A # PARTICULAR PURPOSE. m4_include([acinclude.m4]) tsung-1.8.0/acinclude.m40000755000201100017670000000255314377756736014577 0ustar nniclausdreamdnl as-ac-expand.m4 0.2.0 -*- autoconf -*- dnl autostars m4 macro for expanding directories using configure's prefix dnl (C) 2003, 2004, 2005 Thomas Vander Stichele dnl Copying and distribution of this file, with or without modification, dnl are permitted in any medium without royalty provided the copyright dnl notice and this notice are preserved. dnl AS_AC_EXPAND(VAR, CONFIGURE_VAR) dnl example: dnl AS_AC_EXPAND(SYSCONFDIR, $sysconfdir) dnl will set SYSCONFDIR to /usr/local/etc if prefix=/usr/local AC_DEFUN([AS_AC_EXPAND], [ EXP_VAR=[$1] FROM_VAR=[$2] dnl first expand prefix and exec_prefix if necessary prefix_save=$prefix exec_prefix_save=$exec_prefix dnl if no prefix given, then use /usr/local, the default prefix if test "x$prefix" = "xNONE"; then prefix="$ac_default_prefix" fi dnl if no exec_prefix given, then use prefix if test "x$exec_prefix" = "xNONE"; then exec_prefix=$prefix fi full_var="$FROM_VAR" dnl loop until it doesn't change anymore while true; do new_full_var="`eval echo $full_var`" if test "x$new_full_var" = "x$full_var"; then break; fi full_var=$new_full_var done dnl clean up full_var=$new_full_var AC_SUBST([$1], "$full_var") dnl restore prefix and exec_prefix prefix=$prefix_save exec_prefix=$exec_prefix_save ]) tsung-1.8.0/config.guess0000644000201100017670000012450114377756736014716 0ustar nniclausdream#! /bin/sh # Attempt to guess a canonical system name. # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, # 2000, 2001, 2002, 2003, 2004 Free Software Foundation, Inc. timestamp='2004-09-07' # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # As a special exception to the GNU General Public License, if you # distribute this file as part of a program that contains a # configuration script generated by Autoconf, you may include it under # the same distribution terms that you use for the rest of that program. # Originally written by Per Bothner . # Please send patches to . Submit a context # diff and a properly formatted ChangeLog entry. # # This script attempts to guess a canonical system name similar to # config.sub. If it succeeds, it prints the system name on stdout, and # exits with 0. Otherwise, it exits with 1. # # The plan is that this can be called by configure scripts if you # don't specify an explicit build system type. me=`echo "$0" | sed -e 's,.*/,,'` usage="\ Usage: $0 [OPTION] Output the configuration name of the system \`$me' is run on. Operation modes: -h, --help print this help, then exit -t, --time-stamp print date of last modification, then exit -v, --version print version number, then exit Report bugs and patches to ." version="\ GNU config.guess ($timestamp) Originally written by Per Bothner. Copyright (C) 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." help=" Try \`$me --help' for more information." # Parse command line while test $# -gt 0 ; do case $1 in --time-stamp | --time* | -t ) echo "$timestamp" ; exit 0 ;; --version | -v ) echo "$version" ; exit 0 ;; --help | --h* | -h ) echo "$usage"; exit 0 ;; -- ) # Stop option processing shift; break ;; - ) # Use stdin as input. break ;; -* ) echo "$me: invalid option $1$help" >&2 exit 1 ;; * ) break ;; esac done if test $# != 0; then echo "$me: too many arguments$help" >&2 exit 1 fi trap 'exit 1' 1 2 15 # CC_FOR_BUILD -- compiler used by this script. Note that the use of a # compiler to aid in system detection is discouraged as it requires # temporary files to be created and, as you can see below, it is a # headache to deal with in a portable fashion. # Historically, `CC_FOR_BUILD' used to be named `HOST_CC'. We still # use `HOST_CC' if defined, but it is deprecated. # Portable tmp directory creation inspired by the Autoconf team. set_cc_for_build=' trap "exitcode=\$?; (rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null) && exit \$exitcode" 0 ; trap "rm -f \$tmpfiles 2>/dev/null; rmdir \$tmp 2>/dev/null; exit 1" 1 2 13 15 ; : ${TMPDIR=/tmp} ; { tmp=`(umask 077 && mktemp -d -q "$TMPDIR/cgXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" ; } || { test -n "$RANDOM" && tmp=$TMPDIR/cg$$-$RANDOM && (umask 077 && mkdir $tmp) ; } || { tmp=$TMPDIR/cg-$$ && (umask 077 && mkdir $tmp) && echo "Warning: creating insecure temp directory" >&2 ; } || { echo "$me: cannot create a temporary directory in $TMPDIR" >&2 ; exit 1 ; } ; dummy=$tmp/dummy ; tmpfiles="$dummy.c $dummy.o $dummy.rel $dummy" ; case $CC_FOR_BUILD,$HOST_CC,$CC in ,,) echo "int x;" > $dummy.c ; for c in cc gcc c89 c99 ; do if ($c -c -o $dummy.o $dummy.c) >/dev/null 2>&1 ; then CC_FOR_BUILD="$c"; break ; fi ; done ; if test x"$CC_FOR_BUILD" = x ; then CC_FOR_BUILD=no_compiler_found ; fi ;; ,,*) CC_FOR_BUILD=$CC ;; ,*,*) CC_FOR_BUILD=$HOST_CC ;; esac ;' # This is needed to find uname on a Pyramid OSx when run in the BSD universe. # (ghazi@noc.rutgers.edu 1994-08-24) if (test -f /.attbin/uname) >/dev/null 2>&1 ; then PATH=$PATH:/.attbin ; export PATH fi UNAME_MACHINE=`(uname -m) 2>/dev/null` || UNAME_MACHINE=unknown UNAME_RELEASE=`(uname -r) 2>/dev/null` || UNAME_RELEASE=unknown UNAME_SYSTEM=`(uname -s) 2>/dev/null` || UNAME_SYSTEM=unknown UNAME_VERSION=`(uname -v) 2>/dev/null` || UNAME_VERSION=unknown # Note: order is significant - the case branches are not exclusive. case "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" in *:NetBSD:*:*) # NetBSD (nbsd) targets should (where applicable) match one or # more of the tuples: *-*-netbsdelf*, *-*-netbsdaout*, # *-*-netbsdecoff* and *-*-netbsd*. For targets that recently # switched to ELF, *-*-netbsd* would select the old # object file format. This provides both forward # compatibility and a consistent mechanism for selecting the # object file format. # # Note: NetBSD doesn't particularly care about the vendor # portion of the name. We always set it to "unknown". sysctl="sysctl -n hw.machine_arch" UNAME_MACHINE_ARCH=`(/sbin/$sysctl 2>/dev/null || \ /usr/sbin/$sysctl 2>/dev/null || echo unknown)` case "${UNAME_MACHINE_ARCH}" in armeb) machine=armeb-unknown ;; arm*) machine=arm-unknown ;; sh3el) machine=shl-unknown ;; sh3eb) machine=sh-unknown ;; *) machine=${UNAME_MACHINE_ARCH}-unknown ;; esac # The Operating System including object format, if it has switched # to ELF recently, or will in the future. case "${UNAME_MACHINE_ARCH}" in arm*|i386|m68k|ns32k|sh3*|sparc|vax) eval $set_cc_for_build if echo __ELF__ | $CC_FOR_BUILD -E - 2>/dev/null \ | grep __ELF__ >/dev/null then # Once all utilities can be ECOFF (netbsdecoff) or a.out (netbsdaout). # Return netbsd for either. FIX? os=netbsd else os=netbsdelf fi ;; *) os=netbsd ;; esac # The OS release # Debian GNU/NetBSD machines have a different userland, and # thus, need a distinct triplet. However, they do not need # kernel version information, so it can be replaced with a # suitable tag, in the style of linux-gnu. case "${UNAME_VERSION}" in Debian*) release='-gnu' ;; *) release=`echo ${UNAME_RELEASE}|sed -e 's/[-_].*/\./'` ;; esac # Since CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM: # contains redundant information, the shorter form: # CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM is used. echo "${machine}-${os}${release}" exit 0 ;; amd64:OpenBSD:*:*) echo x86_64-unknown-openbsd${UNAME_RELEASE} exit 0 ;; amiga:OpenBSD:*:*) echo m68k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; cats:OpenBSD:*:*) echo arm-unknown-openbsd${UNAME_RELEASE} exit 0 ;; hp300:OpenBSD:*:*) echo m68k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; luna88k:OpenBSD:*:*) echo m88k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; mac68k:OpenBSD:*:*) echo m68k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; macppc:OpenBSD:*:*) echo powerpc-unknown-openbsd${UNAME_RELEASE} exit 0 ;; mvme68k:OpenBSD:*:*) echo m68k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; mvme88k:OpenBSD:*:*) echo m88k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; mvmeppc:OpenBSD:*:*) echo powerpc-unknown-openbsd${UNAME_RELEASE} exit 0 ;; sgi:OpenBSD:*:*) echo mips64-unknown-openbsd${UNAME_RELEASE} exit 0 ;; sun3:OpenBSD:*:*) echo m68k-unknown-openbsd${UNAME_RELEASE} exit 0 ;; *:OpenBSD:*:*) echo ${UNAME_MACHINE}-unknown-openbsd${UNAME_RELEASE} exit 0 ;; *:ekkoBSD:*:*) echo ${UNAME_MACHINE}-unknown-ekkobsd${UNAME_RELEASE} exit 0 ;; macppc:MirBSD:*:*) echo powerppc-unknown-mirbsd${UNAME_RELEASE} exit 0 ;; *:MirBSD:*:*) echo ${UNAME_MACHINE}-unknown-mirbsd${UNAME_RELEASE} exit 0 ;; alpha:OSF1:*:*) case $UNAME_RELEASE in *4.0) UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $3}'` ;; *5.*) UNAME_RELEASE=`/usr/sbin/sizer -v | awk '{print $4}'` ;; esac # According to Compaq, /usr/sbin/psrinfo has been available on # OSF/1 and Tru64 systems produced since 1995. I hope that # covers most systems running today. This code pipes the CPU # types through head -n 1, so we only detect the type of CPU 0. ALPHA_CPU_TYPE=`/usr/sbin/psrinfo -v | sed -n -e 's/^ The alpha \(.*\) processor.*$/\1/p' | head -n 1` case "$ALPHA_CPU_TYPE" in "EV4 (21064)") UNAME_MACHINE="alpha" ;; "EV4.5 (21064)") UNAME_MACHINE="alpha" ;; "LCA4 (21066/21068)") UNAME_MACHINE="alpha" ;; "EV5 (21164)") UNAME_MACHINE="alphaev5" ;; "EV5.6 (21164A)") UNAME_MACHINE="alphaev56" ;; "EV5.6 (21164PC)") UNAME_MACHINE="alphapca56" ;; "EV5.7 (21164PC)") UNAME_MACHINE="alphapca57" ;; "EV6 (21264)") UNAME_MACHINE="alphaev6" ;; "EV6.7 (21264A)") UNAME_MACHINE="alphaev67" ;; "EV6.8CB (21264C)") UNAME_MACHINE="alphaev68" ;; "EV6.8AL (21264B)") UNAME_MACHINE="alphaev68" ;; "EV6.8CX (21264D)") UNAME_MACHINE="alphaev68" ;; "EV6.9A (21264/EV69A)") UNAME_MACHINE="alphaev69" ;; "EV7 (21364)") UNAME_MACHINE="alphaev7" ;; "EV7.9 (21364A)") UNAME_MACHINE="alphaev79" ;; esac # A Pn.n version is a patched version. # A Vn.n version is a released version. # A Tn.n version is a released field test version. # A Xn.n version is an unreleased experimental baselevel. # 1.2 uses "1.2" for uname -r. echo ${UNAME_MACHINE}-dec-osf`echo ${UNAME_RELEASE} | sed -e 's/^[PVTX]//' | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` exit 0 ;; Alpha\ *:Windows_NT*:*) # How do we know it's Interix rather than the generic POSIX subsystem? # Should we change UNAME_MACHINE based on the output of uname instead # of the specific Alpha model? echo alpha-pc-interix exit 0 ;; 21064:Windows_NT:50:3) echo alpha-dec-winnt3.5 exit 0 ;; Amiga*:UNIX_System_V:4.0:*) echo m68k-unknown-sysv4 exit 0;; *:[Aa]miga[Oo][Ss]:*:*) echo ${UNAME_MACHINE}-unknown-amigaos exit 0 ;; *:[Mm]orph[Oo][Ss]:*:*) echo ${UNAME_MACHINE}-unknown-morphos exit 0 ;; *:OS/390:*:*) echo i370-ibm-openedition exit 0 ;; *:OS400:*:*) echo powerpc-ibm-os400 exit 0 ;; arm:RISC*:1.[012]*:*|arm:riscix:1.[012]*:*) echo arm-acorn-riscix${UNAME_RELEASE} exit 0;; SR2?01:HI-UX/MPP:*:* | SR8000:HI-UX/MPP:*:*) echo hppa1.1-hitachi-hiuxmpp exit 0;; Pyramid*:OSx*:*:* | MIS*:OSx*:*:* | MIS*:SMP_DC-OSx*:*:*) # akee@wpdis03.wpafb.af.mil (Earle F. Ake) contributed MIS and NILE. if test "`(/bin/universe) 2>/dev/null`" = att ; then echo pyramid-pyramid-sysv3 else echo pyramid-pyramid-bsd fi exit 0 ;; NILE*:*:*:dcosx) echo pyramid-pyramid-svr4 exit 0 ;; DRS?6000:unix:4.0:6*) echo sparc-icl-nx6 exit 0 ;; DRS?6000:UNIX_SV:4.2*:7*) case `/usr/bin/uname -p` in sparc) echo sparc-icl-nx7 && exit 0 ;; esac ;; sun4H:SunOS:5.*:*) echo sparc-hal-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` exit 0 ;; sun4*:SunOS:5.*:* | tadpole*:SunOS:5.*:*) echo sparc-sun-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` exit 0 ;; i86pc:SunOS:5.*:*) echo i386-pc-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` exit 0 ;; sun4*:SunOS:6*:*) # According to config.sub, this is the proper way to canonicalize # SunOS6. Hard to guess exactly what SunOS6 will be like, but # it's likely to be more like Solaris than SunOS4. echo sparc-sun-solaris3`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` exit 0 ;; sun4*:SunOS:*:*) case "`/usr/bin/arch -k`" in Series*|S4*) UNAME_RELEASE=`uname -v` ;; esac # Japanese Language versions have a version number like `4.1.3-JL'. echo sparc-sun-sunos`echo ${UNAME_RELEASE}|sed -e 's/-/_/'` exit 0 ;; sun3*:SunOS:*:*) echo m68k-sun-sunos${UNAME_RELEASE} exit 0 ;; sun*:*:4.2BSD:*) UNAME_RELEASE=`(sed 1q /etc/motd | awk '{print substr($5,1,3)}') 2>/dev/null` test "x${UNAME_RELEASE}" = "x" && UNAME_RELEASE=3 case "`/bin/arch`" in sun3) echo m68k-sun-sunos${UNAME_RELEASE} ;; sun4) echo sparc-sun-sunos${UNAME_RELEASE} ;; esac exit 0 ;; aushp:SunOS:*:*) echo sparc-auspex-sunos${UNAME_RELEASE} exit 0 ;; # The situation for MiNT is a little confusing. The machine name # can be virtually everything (everything which is not # "atarist" or "atariste" at least should have a processor # > m68000). The system name ranges from "MiNT" over "FreeMiNT" # to the lowercase version "mint" (or "freemint"). Finally # the system name "TOS" denotes a system which is actually not # MiNT. But MiNT is downward compatible to TOS, so this should # be no problem. atarist[e]:*MiNT:*:* | atarist[e]:*mint:*:* | atarist[e]:*TOS:*:*) echo m68k-atari-mint${UNAME_RELEASE} exit 0 ;; atari*:*MiNT:*:* | atari*:*mint:*:* | atarist[e]:*TOS:*:*) echo m68k-atari-mint${UNAME_RELEASE} exit 0 ;; *falcon*:*MiNT:*:* | *falcon*:*mint:*:* | *falcon*:*TOS:*:*) echo m68k-atari-mint${UNAME_RELEASE} exit 0 ;; milan*:*MiNT:*:* | milan*:*mint:*:* | *milan*:*TOS:*:*) echo m68k-milan-mint${UNAME_RELEASE} exit 0 ;; hades*:*MiNT:*:* | hades*:*mint:*:* | *hades*:*TOS:*:*) echo m68k-hades-mint${UNAME_RELEASE} exit 0 ;; *:*MiNT:*:* | *:*mint:*:* | *:*TOS:*:*) echo m68k-unknown-mint${UNAME_RELEASE} exit 0 ;; m68k:machten:*:*) echo m68k-apple-machten${UNAME_RELEASE} exit 0 ;; powerpc:machten:*:*) echo powerpc-apple-machten${UNAME_RELEASE} exit 0 ;; RISC*:Mach:*:*) echo mips-dec-mach_bsd4.3 exit 0 ;; RISC*:ULTRIX:*:*) echo mips-dec-ultrix${UNAME_RELEASE} exit 0 ;; VAX*:ULTRIX*:*:*) echo vax-dec-ultrix${UNAME_RELEASE} exit 0 ;; 2020:CLIX:*:* | 2430:CLIX:*:*) echo clipper-intergraph-clix${UNAME_RELEASE} exit 0 ;; mips:*:*:UMIPS | mips:*:*:RISCos) eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #ifdef __cplusplus #include /* for printf() prototype */ int main (int argc, char *argv[]) { #else int main (argc, argv) int argc; char *argv[]; { #endif #if defined (host_mips) && defined (MIPSEB) #if defined (SYSTYPE_SYSV) printf ("mips-mips-riscos%ssysv\n", argv[1]); exit (0); #endif #if defined (SYSTYPE_SVR4) printf ("mips-mips-riscos%ssvr4\n", argv[1]); exit (0); #endif #if defined (SYSTYPE_BSD43) || defined(SYSTYPE_BSD) printf ("mips-mips-riscos%sbsd\n", argv[1]); exit (0); #endif #endif exit (-1); } EOF $CC_FOR_BUILD -o $dummy $dummy.c \ && $dummy `echo "${UNAME_RELEASE}" | sed -n 's/\([0-9]*\).*/\1/p'` \ && exit 0 echo mips-mips-riscos${UNAME_RELEASE} exit 0 ;; Motorola:PowerMAX_OS:*:*) echo powerpc-motorola-powermax exit 0 ;; Motorola:*:4.3:PL8-*) echo powerpc-harris-powermax exit 0 ;; Night_Hawk:*:*:PowerMAX_OS | Synergy:PowerMAX_OS:*:*) echo powerpc-harris-powermax exit 0 ;; Night_Hawk:Power_UNIX:*:*) echo powerpc-harris-powerunix exit 0 ;; m88k:CX/UX:7*:*) echo m88k-harris-cxux7 exit 0 ;; m88k:*:4*:R4*) echo m88k-motorola-sysv4 exit 0 ;; m88k:*:3*:R3*) echo m88k-motorola-sysv3 exit 0 ;; AViiON:dgux:*:*) # DG/UX returns AViiON for all architectures UNAME_PROCESSOR=`/usr/bin/uname -p` if [ $UNAME_PROCESSOR = mc88100 ] || [ $UNAME_PROCESSOR = mc88110 ] then if [ ${TARGET_BINARY_INTERFACE}x = m88kdguxelfx ] || \ [ ${TARGET_BINARY_INTERFACE}x = x ] then echo m88k-dg-dgux${UNAME_RELEASE} else echo m88k-dg-dguxbcs${UNAME_RELEASE} fi else echo i586-dg-dgux${UNAME_RELEASE} fi exit 0 ;; M88*:DolphinOS:*:*) # DolphinOS (SVR3) echo m88k-dolphin-sysv3 exit 0 ;; M88*:*:R3*:*) # Delta 88k system running SVR3 echo m88k-motorola-sysv3 exit 0 ;; XD88*:*:*:*) # Tektronix XD88 system running UTekV (SVR3) echo m88k-tektronix-sysv3 exit 0 ;; Tek43[0-9][0-9]:UTek:*:*) # Tektronix 4300 system running UTek (BSD) echo m68k-tektronix-bsd exit 0 ;; *:IRIX*:*:*) echo mips-sgi-irix`echo ${UNAME_RELEASE}|sed -e 's/-/_/g'` exit 0 ;; ????????:AIX?:[12].1:2) # AIX 2.2.1 or AIX 2.1.1 is RT/PC AIX. echo romp-ibm-aix # uname -m gives an 8 hex-code CPU id exit 0 ;; # Note that: echo "'`uname -s`'" gives 'AIX ' i*86:AIX:*:*) echo i386-ibm-aix exit 0 ;; ia64:AIX:*:*) if [ -x /usr/bin/oslevel ] ; then IBM_REV=`/usr/bin/oslevel` else IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE} fi echo ${UNAME_MACHINE}-ibm-aix${IBM_REV} exit 0 ;; *:AIX:2:3) if grep bos325 /usr/include/stdio.h >/dev/null 2>&1; then eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #include main() { if (!__power_pc()) exit(1); puts("powerpc-ibm-aix3.2.5"); exit(0); } EOF $CC_FOR_BUILD -o $dummy $dummy.c && $dummy && exit 0 echo rs6000-ibm-aix3.2.5 elif grep bos324 /usr/include/stdio.h >/dev/null 2>&1; then echo rs6000-ibm-aix3.2.4 else echo rs6000-ibm-aix3.2 fi exit 0 ;; *:AIX:*:[45]) IBM_CPU_ID=`/usr/sbin/lsdev -C -c processor -S available | sed 1q | awk '{ print $1 }'` if /usr/sbin/lsattr -El ${IBM_CPU_ID} | grep ' POWER' >/dev/null 2>&1; then IBM_ARCH=rs6000 else IBM_ARCH=powerpc fi if [ -x /usr/bin/oslevel ] ; then IBM_REV=`/usr/bin/oslevel` else IBM_REV=${UNAME_VERSION}.${UNAME_RELEASE} fi echo ${IBM_ARCH}-ibm-aix${IBM_REV} exit 0 ;; *:AIX:*:*) echo rs6000-ibm-aix exit 0 ;; ibmrt:4.4BSD:*|romp-ibm:BSD:*) echo romp-ibm-bsd4.4 exit 0 ;; ibmrt:*BSD:*|romp-ibm:BSD:*) # covers RT/PC BSD and echo romp-ibm-bsd${UNAME_RELEASE} # 4.3 with uname added to exit 0 ;; # report: romp-ibm BSD 4.3 *:BOSX:*:*) echo rs6000-bull-bosx exit 0 ;; DPX/2?00:B.O.S.:*:*) echo m68k-bull-sysv3 exit 0 ;; 9000/[34]??:4.3bsd:1.*:*) echo m68k-hp-bsd exit 0 ;; hp300:4.4BSD:*:* | 9000/[34]??:4.3bsd:2.*:*) echo m68k-hp-bsd4.4 exit 0 ;; 9000/[34678]??:HP-UX:*:*) HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'` case "${UNAME_MACHINE}" in 9000/31? ) HP_ARCH=m68000 ;; 9000/[34]?? ) HP_ARCH=m68k ;; 9000/[678][0-9][0-9]) if [ -x /usr/bin/getconf ]; then sc_cpu_version=`/usr/bin/getconf SC_CPU_VERSION 2>/dev/null` sc_kernel_bits=`/usr/bin/getconf SC_KERNEL_BITS 2>/dev/null` case "${sc_cpu_version}" in 523) HP_ARCH="hppa1.0" ;; # CPU_PA_RISC1_0 528) HP_ARCH="hppa1.1" ;; # CPU_PA_RISC1_1 532) # CPU_PA_RISC2_0 case "${sc_kernel_bits}" in 32) HP_ARCH="hppa2.0n" ;; 64) HP_ARCH="hppa2.0w" ;; '') HP_ARCH="hppa2.0" ;; # HP-UX 10.20 esac ;; esac fi if [ "${HP_ARCH}" = "" ]; then eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #define _HPUX_SOURCE #include #include int main () { #if defined(_SC_KERNEL_BITS) long bits = sysconf(_SC_KERNEL_BITS); #endif long cpu = sysconf (_SC_CPU_VERSION); switch (cpu) { case CPU_PA_RISC1_0: puts ("hppa1.0"); break; case CPU_PA_RISC1_1: puts ("hppa1.1"); break; case CPU_PA_RISC2_0: #if defined(_SC_KERNEL_BITS) switch (bits) { case 64: puts ("hppa2.0w"); break; case 32: puts ("hppa2.0n"); break; default: puts ("hppa2.0"); break; } break; #else /* !defined(_SC_KERNEL_BITS) */ puts ("hppa2.0"); break; #endif default: puts ("hppa1.0"); break; } exit (0); } EOF (CCOPTS= $CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null) && HP_ARCH=`$dummy` test -z "$HP_ARCH" && HP_ARCH=hppa fi ;; esac if [ ${HP_ARCH} = "hppa2.0w" ] then # avoid double evaluation of $set_cc_for_build test -n "$CC_FOR_BUILD" || eval $set_cc_for_build if echo __LP64__ | (CCOPTS= $CC_FOR_BUILD -E -) | grep __LP64__ >/dev/null then HP_ARCH="hppa2.0w" else HP_ARCH="hppa64" fi fi echo ${HP_ARCH}-hp-hpux${HPUX_REV} exit 0 ;; ia64:HP-UX:*:*) HPUX_REV=`echo ${UNAME_RELEASE}|sed -e 's/[^.]*.[0B]*//'` echo ia64-hp-hpux${HPUX_REV} exit 0 ;; 3050*:HI-UX:*:*) eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #include int main () { long cpu = sysconf (_SC_CPU_VERSION); /* The order matters, because CPU_IS_HP_MC68K erroneously returns true for CPU_PA_RISC1_0. CPU_IS_PA_RISC returns correct results, however. */ if (CPU_IS_PA_RISC (cpu)) { switch (cpu) { case CPU_PA_RISC1_0: puts ("hppa1.0-hitachi-hiuxwe2"); break; case CPU_PA_RISC1_1: puts ("hppa1.1-hitachi-hiuxwe2"); break; case CPU_PA_RISC2_0: puts ("hppa2.0-hitachi-hiuxwe2"); break; default: puts ("hppa-hitachi-hiuxwe2"); break; } } else if (CPU_IS_HP_MC68K (cpu)) puts ("m68k-hitachi-hiuxwe2"); else puts ("unknown-hitachi-hiuxwe2"); exit (0); } EOF $CC_FOR_BUILD -o $dummy $dummy.c && $dummy && exit 0 echo unknown-hitachi-hiuxwe2 exit 0 ;; 9000/7??:4.3bsd:*:* | 9000/8?[79]:4.3bsd:*:* ) echo hppa1.1-hp-bsd exit 0 ;; 9000/8??:4.3bsd:*:*) echo hppa1.0-hp-bsd exit 0 ;; *9??*:MPE/iX:*:* | *3000*:MPE/iX:*:*) echo hppa1.0-hp-mpeix exit 0 ;; hp7??:OSF1:*:* | hp8?[79]:OSF1:*:* ) echo hppa1.1-hp-osf exit 0 ;; hp8??:OSF1:*:*) echo hppa1.0-hp-osf exit 0 ;; i*86:OSF1:*:*) if [ -x /usr/sbin/sysversion ] ; then echo ${UNAME_MACHINE}-unknown-osf1mk else echo ${UNAME_MACHINE}-unknown-osf1 fi exit 0 ;; parisc*:Lites*:*:*) echo hppa1.1-hp-lites exit 0 ;; C1*:ConvexOS:*:* | convex:ConvexOS:C1*:*) echo c1-convex-bsd exit 0 ;; C2*:ConvexOS:*:* | convex:ConvexOS:C2*:*) if getsysinfo -f scalar_acc then echo c32-convex-bsd else echo c2-convex-bsd fi exit 0 ;; C34*:ConvexOS:*:* | convex:ConvexOS:C34*:*) echo c34-convex-bsd exit 0 ;; C38*:ConvexOS:*:* | convex:ConvexOS:C38*:*) echo c38-convex-bsd exit 0 ;; C4*:ConvexOS:*:* | convex:ConvexOS:C4*:*) echo c4-convex-bsd exit 0 ;; CRAY*Y-MP:*:*:*) echo ymp-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' exit 0 ;; CRAY*[A-Z]90:*:*:*) echo ${UNAME_MACHINE}-cray-unicos${UNAME_RELEASE} \ | sed -e 's/CRAY.*\([A-Z]90\)/\1/' \ -e y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/ \ -e 's/\.[^.]*$/.X/' exit 0 ;; CRAY*TS:*:*:*) echo t90-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' exit 0 ;; CRAY*T3E:*:*:*) echo alphaev5-cray-unicosmk${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' exit 0 ;; CRAY*SV1:*:*:*) echo sv1-cray-unicos${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' exit 0 ;; *:UNICOS/mp:*:*) echo craynv-cray-unicosmp${UNAME_RELEASE} | sed -e 's/\.[^.]*$/.X/' exit 0 ;; F30[01]:UNIX_System_V:*:* | F700:UNIX_System_V:*:*) FUJITSU_PROC=`uname -m | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz'` FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` FUJITSU_REL=`echo ${UNAME_RELEASE} | sed -e 's/ /_/'` echo "${FUJITSU_PROC}-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" exit 0 ;; 5000:UNIX_System_V:4.*:*) FUJITSU_SYS=`uname -p | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/\///'` FUJITSU_REL=`echo ${UNAME_RELEASE} | tr 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 'abcdefghijklmnopqrstuvwxyz' | sed -e 's/ /_/'` echo "sparc-fujitsu-${FUJITSU_SYS}${FUJITSU_REL}" exit 0 ;; i*86:BSD/386:*:* | i*86:BSD/OS:*:* | *:Ascend\ Embedded/OS:*:*) echo ${UNAME_MACHINE}-pc-bsdi${UNAME_RELEASE} exit 0 ;; sparc*:BSD/OS:*:*) echo sparc-unknown-bsdi${UNAME_RELEASE} exit 0 ;; *:BSD/OS:*:*) echo ${UNAME_MACHINE}-unknown-bsdi${UNAME_RELEASE} exit 0 ;; *:FreeBSD:*:*) echo ${UNAME_MACHINE}-unknown-freebsd`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` exit 0 ;; i*:CYGWIN*:*) echo ${UNAME_MACHINE}-pc-cygwin exit 0 ;; i*:MINGW*:*) echo ${UNAME_MACHINE}-pc-mingw32 exit 0 ;; i*:PW*:*) echo ${UNAME_MACHINE}-pc-pw32 exit 0 ;; x86:Interix*:[34]*) echo i586-pc-interix${UNAME_RELEASE}|sed -e 's/\..*//' exit 0 ;; [345]86:Windows_95:* | [345]86:Windows_98:* | [345]86:Windows_NT:*) echo i${UNAME_MACHINE}-pc-mks exit 0 ;; i*:Windows_NT*:* | Pentium*:Windows_NT*:*) # How do we know it's Interix rather than the generic POSIX subsystem? # It also conflicts with pre-2.0 versions of AT&T UWIN. Should we # UNAME_MACHINE based on the output of uname instead of i386? echo i586-pc-interix exit 0 ;; i*:UWIN*:*) echo ${UNAME_MACHINE}-pc-uwin exit 0 ;; p*:CYGWIN*:*) echo powerpcle-unknown-cygwin exit 0 ;; prep*:SunOS:5.*:*) echo powerpcle-unknown-solaris2`echo ${UNAME_RELEASE}|sed -e 's/[^.]*//'` exit 0 ;; *:GNU:*:*) # the GNU system echo `echo ${UNAME_MACHINE}|sed -e 's,[-/].*$,,'`-unknown-gnu`echo ${UNAME_RELEASE}|sed -e 's,/.*$,,'` exit 0 ;; *:GNU/*:*:*) # other systems with GNU libc and userland echo ${UNAME_MACHINE}-unknown-`echo ${UNAME_SYSTEM} | sed 's,^[^/]*/,,' | tr '[A-Z]' '[a-z]'``echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'`-gnu exit 0 ;; i*86:Minix:*:*) echo ${UNAME_MACHINE}-pc-minix exit 0 ;; arm*:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; cris:Linux:*:*) echo cris-axis-linux-gnu exit 0 ;; crisv32:Linux:*:*) echo crisv32-axis-linux-gnu exit 0 ;; frv:Linux:*:*) echo frv-unknown-linux-gnu exit 0 ;; ia64:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; m32r*:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; m68*:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; mips:Linux:*:*) eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #undef CPU #undef mips #undef mipsel #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) CPU=mipsel #else #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) CPU=mips #else CPU= #endif #endif EOF eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep ^CPU=` test x"${CPU}" != x && echo "${CPU}-unknown-linux-gnu" && exit 0 ;; mips64:Linux:*:*) eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #undef CPU #undef mips64 #undef mips64el #if defined(__MIPSEL__) || defined(__MIPSEL) || defined(_MIPSEL) || defined(MIPSEL) CPU=mips64el #else #if defined(__MIPSEB__) || defined(__MIPSEB) || defined(_MIPSEB) || defined(MIPSEB) CPU=mips64 #else CPU= #endif #endif EOF eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep ^CPU=` test x"${CPU}" != x && echo "${CPU}-unknown-linux-gnu" && exit 0 ;; ppc:Linux:*:*) echo powerpc-unknown-linux-gnu exit 0 ;; ppc64:Linux:*:*) echo powerpc64-unknown-linux-gnu exit 0 ;; alpha:Linux:*:*) case `sed -n '/^cpu model/s/^.*: \(.*\)/\1/p' < /proc/cpuinfo` in EV5) UNAME_MACHINE=alphaev5 ;; EV56) UNAME_MACHINE=alphaev56 ;; PCA56) UNAME_MACHINE=alphapca56 ;; PCA57) UNAME_MACHINE=alphapca56 ;; EV6) UNAME_MACHINE=alphaev6 ;; EV67) UNAME_MACHINE=alphaev67 ;; EV68*) UNAME_MACHINE=alphaev68 ;; esac objdump --private-headers /bin/sh | grep ld.so.1 >/dev/null if test "$?" = 0 ; then LIBC="libc1" ; else LIBC="" ; fi echo ${UNAME_MACHINE}-unknown-linux-gnu${LIBC} exit 0 ;; parisc:Linux:*:* | hppa:Linux:*:*) # Look for CPU level case `grep '^cpu[^a-z]*:' /proc/cpuinfo 2>/dev/null | cut -d' ' -f2` in PA7*) echo hppa1.1-unknown-linux-gnu ;; PA8*) echo hppa2.0-unknown-linux-gnu ;; *) echo hppa-unknown-linux-gnu ;; esac exit 0 ;; parisc64:Linux:*:* | hppa64:Linux:*:*) echo hppa64-unknown-linux-gnu exit 0 ;; s390:Linux:*:* | s390x:Linux:*:*) echo ${UNAME_MACHINE}-ibm-linux exit 0 ;; sh64*:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; sh*:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; sparc:Linux:*:* | sparc64:Linux:*:*) echo ${UNAME_MACHINE}-unknown-linux-gnu exit 0 ;; x86_64:Linux:*:*) echo x86_64-unknown-linux-gnu exit 0 ;; i*86:Linux:*:*) # The BFD linker knows what the default object file format is, so # first see if it will tell us. cd to the root directory to prevent # problems with other programs or directories called `ld' in the path. # Set LC_ALL=C to ensure ld outputs messages in English. ld_supported_targets=`cd /; LC_ALL=C ld --help 2>&1 \ | sed -ne '/supported targets:/!d s/[ ][ ]*/ /g s/.*supported targets: *// s/ .*// p'` case "$ld_supported_targets" in elf32-i386) TENTATIVE="${UNAME_MACHINE}-pc-linux-gnu" ;; a.out-i386-linux) echo "${UNAME_MACHINE}-pc-linux-gnuaout" exit 0 ;; coff-i386) echo "${UNAME_MACHINE}-pc-linux-gnucoff" exit 0 ;; "") # Either a pre-BFD a.out linker (linux-gnuoldld) or # one that does not give us useful --help. echo "${UNAME_MACHINE}-pc-linux-gnuoldld" exit 0 ;; esac # Determine whether the default compiler is a.out or elf eval $set_cc_for_build sed 's/^ //' << EOF >$dummy.c #include #ifdef __ELF__ # ifdef __GLIBC__ # if __GLIBC__ >= 2 LIBC=gnu # else LIBC=gnulibc1 # endif # else LIBC=gnulibc1 # endif #else #ifdef __INTEL_COMPILER LIBC=gnu #else LIBC=gnuaout #endif #endif #ifdef __dietlibc__ LIBC=dietlibc #endif EOF eval `$CC_FOR_BUILD -E $dummy.c 2>/dev/null | grep ^LIBC=` test x"${LIBC}" != x && echo "${UNAME_MACHINE}-pc-linux-${LIBC}" && exit 0 test x"${TENTATIVE}" != x && echo "${TENTATIVE}" && exit 0 ;; i*86:DYNIX/ptx:4*:*) # ptx 4.0 does uname -s correctly, with DYNIX/ptx in there. # earlier versions are messed up and put the nodename in both # sysname and nodename. echo i386-sequent-sysv4 exit 0 ;; i*86:UNIX_SV:4.2MP:2.*) # Unixware is an offshoot of SVR4, but it has its own version # number series starting with 2... # I am not positive that other SVR4 systems won't match this, # I just have to hope. -- rms. # Use sysv4.2uw... so that sysv4* matches it. echo ${UNAME_MACHINE}-pc-sysv4.2uw${UNAME_VERSION} exit 0 ;; i*86:OS/2:*:*) # If we were able to find `uname', then EMX Unix compatibility # is probably installed. echo ${UNAME_MACHINE}-pc-os2-emx exit 0 ;; i*86:XTS-300:*:STOP) echo ${UNAME_MACHINE}-unknown-stop exit 0 ;; i*86:atheos:*:*) echo ${UNAME_MACHINE}-unknown-atheos exit 0 ;; i*86:syllable:*:*) echo ${UNAME_MACHINE}-pc-syllable exit 0 ;; i*86:LynxOS:2.*:* | i*86:LynxOS:3.[01]*:* | i*86:LynxOS:4.0*:*) echo i386-unknown-lynxos${UNAME_RELEASE} exit 0 ;; i*86:*DOS:*:*) echo ${UNAME_MACHINE}-pc-msdosdjgpp exit 0 ;; i*86:*:4.*:* | i*86:SYSTEM_V:4.*:*) UNAME_REL=`echo ${UNAME_RELEASE} | sed 's/\/MP$//'` if grep Novell /usr/include/link.h >/dev/null 2>/dev/null; then echo ${UNAME_MACHINE}-univel-sysv${UNAME_REL} else echo ${UNAME_MACHINE}-pc-sysv${UNAME_REL} fi exit 0 ;; i*86:*:5:[78]*) case `/bin/uname -X | grep "^Machine"` in *486*) UNAME_MACHINE=i486 ;; *Pentium) UNAME_MACHINE=i586 ;; *Pent*|*Celeron) UNAME_MACHINE=i686 ;; esac echo ${UNAME_MACHINE}-unknown-sysv${UNAME_RELEASE}${UNAME_SYSTEM}${UNAME_VERSION} exit 0 ;; i*86:*:3.2:*) if test -f /usr/options/cb.name; then UNAME_REL=`sed -n 's/.*Version //p' /dev/null >/dev/null ; then UNAME_REL=`(/bin/uname -X|grep Release|sed -e 's/.*= //')` (/bin/uname -X|grep i80486 >/dev/null) && UNAME_MACHINE=i486 (/bin/uname -X|grep '^Machine.*Pentium' >/dev/null) \ && UNAME_MACHINE=i586 (/bin/uname -X|grep '^Machine.*Pent *II' >/dev/null) \ && UNAME_MACHINE=i686 (/bin/uname -X|grep '^Machine.*Pentium Pro' >/dev/null) \ && UNAME_MACHINE=i686 echo ${UNAME_MACHINE}-pc-sco$UNAME_REL else echo ${UNAME_MACHINE}-pc-sysv32 fi exit 0 ;; pc:*:*:*) # Left here for compatibility: # uname -m prints for DJGPP always 'pc', but it prints nothing about # the processor, so we play safe by assuming i386. echo i386-pc-msdosdjgpp exit 0 ;; Intel:Mach:3*:*) echo i386-pc-mach3 exit 0 ;; paragon:*:*:*) echo i860-intel-osf1 exit 0 ;; i860:*:4.*:*) # i860-SVR4 if grep Stardent /usr/include/sys/uadmin.h >/dev/null 2>&1 ; then echo i860-stardent-sysv${UNAME_RELEASE} # Stardent Vistra i860-SVR4 else # Add other i860-SVR4 vendors below as they are discovered. echo i860-unknown-sysv${UNAME_RELEASE} # Unknown i860-SVR4 fi exit 0 ;; mini*:CTIX:SYS*5:*) # "miniframe" echo m68010-convergent-sysv exit 0 ;; mc68k:UNIX:SYSTEM5:3.51m) echo m68k-convergent-sysv exit 0 ;; M680?0:D-NIX:5.3:*) echo m68k-diab-dnix exit 0 ;; M68*:*:R3V[5678]*:*) test -r /sysV68 && echo 'm68k-motorola-sysv' && exit 0 ;; 3[345]??:*:4.0:3.0 | 3[34]??A:*:4.0:3.0 | 3[34]??,*:*:4.0:3.0 | 3[34]??/*:*:4.0:3.0 | 4400:*:4.0:3.0 | 4850:*:4.0:3.0 | SKA40:*:4.0:3.0 | SDS2:*:4.0:3.0 | SHG2:*:4.0:3.0 | S7501*:*:4.0:3.0) OS_REL='' test -r /etc/.relid \ && OS_REL=.`sed -n 's/[^ ]* [^ ]* \([0-9][0-9]\).*/\1/p' < /etc/.relid` /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ && echo i486-ncr-sysv4.3${OS_REL} && exit 0 /bin/uname -p 2>/dev/null | /bin/grep entium >/dev/null \ && echo i586-ncr-sysv4.3${OS_REL} && exit 0 ;; 3[34]??:*:4.0:* | 3[34]??,*:*:4.0:*) /bin/uname -p 2>/dev/null | grep 86 >/dev/null \ && echo i486-ncr-sysv4 && exit 0 ;; m68*:LynxOS:2.*:* | m68*:LynxOS:3.0*:*) echo m68k-unknown-lynxos${UNAME_RELEASE} exit 0 ;; mc68030:UNIX_System_V:4.*:*) echo m68k-atari-sysv4 exit 0 ;; tsung:LynxOS:2.*:*) echo sparc-unknown-lynxos${UNAME_RELEASE} exit 0 ;; rs6000:LynxOS:2.*:*) echo rs6000-unknown-lynxos${UNAME_RELEASE} exit 0 ;; PowerPC:LynxOS:2.*:* | PowerPC:LynxOS:3.[01]*:* | PowerPC:LynxOS:4.0*:*) echo powerpc-unknown-lynxos${UNAME_RELEASE} exit 0 ;; SM[BE]S:UNIX_SV:*:*) echo mips-dde-sysv${UNAME_RELEASE} exit 0 ;; RM*:ReliantUNIX-*:*:*) echo mips-sni-sysv4 exit 0 ;; RM*:SINIX-*:*:*) echo mips-sni-sysv4 exit 0 ;; *:SINIX-*:*:*) if uname -p 2>/dev/null >/dev/null ; then UNAME_MACHINE=`(uname -p) 2>/dev/null` echo ${UNAME_MACHINE}-sni-sysv4 else echo ns32k-sni-sysv fi exit 0 ;; PENTIUM:*:4.0*:*) # Unisys `ClearPath HMP IX 4000' SVR4/MP effort # says echo i586-unisys-sysv4 exit 0 ;; *:UNIX_System_V:4*:FTX*) # From Gerald Hewes . # How about differentiating between stratus architectures? -djm echo hppa1.1-stratus-sysv4 exit 0 ;; *:*:*:FTX*) # From seanf@swdc.stratus.com. echo i860-stratus-sysv4 exit 0 ;; *:VOS:*:*) # From Paul.Green@stratus.com. echo hppa1.1-stratus-vos exit 0 ;; mc68*:A/UX:*:*) echo m68k-apple-aux${UNAME_RELEASE} exit 0 ;; news*:NEWS-OS:6*:*) echo mips-sony-newsos6 exit 0 ;; R[34]000:*System_V*:*:* | R4000:UNIX_SYSV:*:* | R*000:UNIX_SV:*:*) if [ -d /usr/nec ]; then echo mips-nec-sysv${UNAME_RELEASE} else echo mips-unknown-sysv${UNAME_RELEASE} fi exit 0 ;; BeBox:BeOS:*:*) # BeOS running on hardware made by Be, PPC only. echo powerpc-be-beos exit 0 ;; BeMac:BeOS:*:*) # BeOS running on Mac or Mac clone, PPC only. echo powerpc-apple-beos exit 0 ;; BePC:BeOS:*:*) # BeOS running on Intel PC compatible. echo i586-pc-beos exit 0 ;; SX-4:SUPER-UX:*:*) echo sx4-nec-superux${UNAME_RELEASE} exit 0 ;; SX-5:SUPER-UX:*:*) echo sx5-nec-superux${UNAME_RELEASE} exit 0 ;; SX-6:SUPER-UX:*:*) echo sx6-nec-superux${UNAME_RELEASE} exit 0 ;; Power*:Rhapsody:*:*) echo powerpc-apple-rhapsody${UNAME_RELEASE} exit 0 ;; *:Rhapsody:*:*) echo ${UNAME_MACHINE}-apple-rhapsody${UNAME_RELEASE} exit 0 ;; *:Darwin:*:*) UNAME_PROCESSOR=`uname -p` || UNAME_PROCESSOR=unknown case $UNAME_PROCESSOR in *86) UNAME_PROCESSOR=i686 ;; unknown) UNAME_PROCESSOR=powerpc ;; esac echo ${UNAME_PROCESSOR}-apple-darwin${UNAME_RELEASE} exit 0 ;; *:procnto*:*:* | *:QNX:[0123456789]*:*) UNAME_PROCESSOR=`uname -p` if test "$UNAME_PROCESSOR" = "x86"; then UNAME_PROCESSOR=i386 UNAME_MACHINE=pc fi echo ${UNAME_PROCESSOR}-${UNAME_MACHINE}-nto-qnx${UNAME_RELEASE} exit 0 ;; *:QNX:*:4*) echo i386-pc-qnx exit 0 ;; NSR-?:NONSTOP_KERNEL:*:*) echo nsr-tandem-nsk${UNAME_RELEASE} exit 0 ;; *:NonStop-UX:*:*) echo mips-compaq-nonstopux exit 0 ;; BS2000:POSIX*:*:*) echo bs2000-siemens-sysv exit 0 ;; DS/*:UNIX_System_V:*:*) echo ${UNAME_MACHINE}-${UNAME_SYSTEM}-${UNAME_RELEASE} exit 0 ;; *:Plan9:*:*) # "uname -m" is not consistent, so use $cputype instead. 386 # is converted to i386 for consistency with other x86 # operating systems. if test "$cputype" = "386"; then UNAME_MACHINE=i386 else UNAME_MACHINE="$cputype" fi echo ${UNAME_MACHINE}-unknown-plan9 exit 0 ;; *:TOPS-10:*:*) echo pdp10-unknown-tops10 exit 0 ;; *:TENEX:*:*) echo pdp10-unknown-tenex exit 0 ;; KS10:TOPS-20:*:* | KL10:TOPS-20:*:* | TYPE4:TOPS-20:*:*) echo pdp10-dec-tops20 exit 0 ;; XKL-1:TOPS-20:*:* | TYPE5:TOPS-20:*:*) echo pdp10-xkl-tops20 exit 0 ;; *:TOPS-20:*:*) echo pdp10-unknown-tops20 exit 0 ;; *:ITS:*:*) echo pdp10-unknown-its exit 0 ;; SEI:*:*:SEIUX) echo mips-sei-seiux${UNAME_RELEASE} exit 0 ;; *:DragonFly:*:*) echo ${UNAME_MACHINE}-unknown-dragonfly`echo ${UNAME_RELEASE}|sed -e 's/[-(].*//'` exit 0 ;; *:*VMS:*:*) UNAME_MACHINE=`(uname -p) 2>/dev/null` case "${UNAME_MACHINE}" in A*) echo alpha-dec-vms && exit 0 ;; I*) echo ia64-dec-vms && exit 0 ;; V*) echo vax-dec-vms && exit 0 ;; esac esac #echo '(No uname command or uname output not recognized.)' 1>&2 #echo "${UNAME_MACHINE}:${UNAME_SYSTEM}:${UNAME_RELEASE}:${UNAME_VERSION}" 1>&2 eval $set_cc_for_build cat >$dummy.c < # include #endif main () { #if defined (sony) #if defined (MIPSEB) /* BFD wants "bsd" instead of "newsos". Perhaps BFD should be changed, I don't know.... */ printf ("mips-sony-bsd\n"); exit (0); #else #include printf ("m68k-sony-newsos%s\n", #ifdef NEWSOS4 "4" #else "" #endif ); exit (0); #endif #endif #if defined (__arm) && defined (__acorn) && defined (__unix) printf ("arm-acorn-riscix"); exit (0); #endif #if defined (hp300) && !defined (hpux) printf ("m68k-hp-bsd\n"); exit (0); #endif #if defined (NeXT) #if !defined (__ARCHITECTURE__) #define __ARCHITECTURE__ "m68k" #endif int version; version=`(hostinfo | sed -n 's/.*NeXT Mach \([0-9]*\).*/\1/p') 2>/dev/null`; if (version < 4) printf ("%s-next-nextstep%d\n", __ARCHITECTURE__, version); else printf ("%s-next-openstep%d\n", __ARCHITECTURE__, version); exit (0); #endif #if defined (MULTIMAX) || defined (n16) #if defined (UMAXV) printf ("ns32k-encore-sysv\n"); exit (0); #else #if defined (CMU) printf ("ns32k-encore-mach\n"); exit (0); #else printf ("ns32k-encore-bsd\n"); exit (0); #endif #endif #endif #if defined (__386BSD__) printf ("i386-pc-bsd\n"); exit (0); #endif #if defined (sequent) #if defined (i386) printf ("i386-sequent-dynix\n"); exit (0); #endif #if defined (ns32000) printf ("ns32k-sequent-dynix\n"); exit (0); #endif #endif #if defined (_SEQUENT_) struct utsname un; uname(&un); if (strncmp(un.version, "V2", 2) == 0) { printf ("i386-sequent-ptx2\n"); exit (0); } if (strncmp(un.version, "V1", 2) == 0) { /* XXX is V1 correct? */ printf ("i386-sequent-ptx1\n"); exit (0); } printf ("i386-sequent-ptx\n"); exit (0); #endif #if defined (vax) # if !defined (ultrix) # include # if defined (BSD) # if BSD == 43 printf ("vax-dec-bsd4.3\n"); exit (0); # else # if BSD == 199006 printf ("vax-dec-bsd4.3reno\n"); exit (0); # else printf ("vax-dec-bsd\n"); exit (0); # endif # endif # else printf ("vax-dec-bsd\n"); exit (0); # endif # else printf ("vax-dec-ultrix\n"); exit (0); # endif #endif #if defined (alliant) && defined (i860) printf ("i860-alliant-bsd\n"); exit (0); #endif exit (1); } EOF $CC_FOR_BUILD -o $dummy $dummy.c 2>/dev/null && $dummy && exit 0 # Apollos put the system type in the environment. test -d /usr/apollo && { echo ${ISP}-apollo-${SYSTYPE}; exit 0; } # Convex versions that predate uname can use getsysinfo(1) if [ -x /usr/convex/getsysinfo ] then case `getsysinfo -f cpu_type` in c1*) echo c1-convex-bsd exit 0 ;; c2*) if getsysinfo -f scalar_acc then echo c32-convex-bsd else echo c2-convex-bsd fi exit 0 ;; c34*) echo c34-convex-bsd exit 0 ;; c38*) echo c38-convex-bsd exit 0 ;; c4*) echo c4-convex-bsd exit 0 ;; esac fi cat >&2 < in order to provide the needed information to handle your system. config.guess timestamp = $timestamp uname -m = `(uname -m) 2>/dev/null || echo unknown` uname -r = `(uname -r) 2>/dev/null || echo unknown` uname -s = `(uname -s) 2>/dev/null || echo unknown` uname -v = `(uname -v) 2>/dev/null || echo unknown` /usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null` /bin/uname -X = `(/bin/uname -X) 2>/dev/null` hostinfo = `(hostinfo) 2>/dev/null` /bin/universe = `(/bin/universe) 2>/dev/null` /usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null` /bin/arch = `(/bin/arch) 2>/dev/null` /usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null` /usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null` UNAME_MACHINE = ${UNAME_MACHINE} UNAME_RELEASE = ${UNAME_RELEASE} UNAME_SYSTEM = ${UNAME_SYSTEM} UNAME_VERSION = ${UNAME_VERSION} EOF exit 1 # Local variables: # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "timestamp='" # time-stamp-format: "%:y-%02m-%02d" # time-stamp-end: "'" # End: tsung-1.8.0/configure.ac0000644000201100017670000000761514377756736014675 0ustar nniclausdreamdnl DNA define([AC_CACHE_LOAD], )dnl AC_INIT([tsung], m4_normalize(m4_include([vsn.mk])),[tsung-users@process-one.net]) AC_PREREQ(2.59c) AC_COPYRIGHT(Copyright (C) 2008 Nicolas Niclausse) AC_CONFIG_SRCDIR(src/tsung/tsung.erl) dnl AM_INIT_AUTOMAKE() AC_CACHE_LOAD AC_SUBST([CONFIG_STATUS_DEPENDENCIES],[vsn.mk]) AC_SUBST([CONFIGURE_DEPENDENCIES],[vsn.mk]) AC_PATH_PROG(SED, sed) AC_LANG(Erlang) AC_ARG_WITH(erlang, [ --with-erlang=PREFIX path to erlc and erl ]) AC_ERLANG_PATH_ERLC(erlc, $with_erlang:$with_erlang/bin:$PATH) AC_ERLANG_PATH_ERL(erl, $with_erlang:$with_erlang/bin:$PATH) AC_PATH_PROG(DIALYZER,dialyzer, /usr/bin/dializer, $with_erlang:$with_erlang/bin:$PATH) AC_PREFIX_PROGRAM(erl) AC_ERLANG_SUBST_ROOT_DIR() dnl check for xmerl include path AC_ERLANG_CHECK_LIB(xmerl) AC_ERLANG_CHECK_LIB(ssl) AC_ERLANG_CHECK_LIB(crypto) AC_ERLANG_CHECK_LIB(public_key) dnl check if ssl is working AC_CACHE_CHECK([if Erlang/OTP SSL application is running fine], [erlang_cv_ssl_runnable], [erlang_cv_ssl_runnable=no AC_RUN_IFELSE( [AC_LANG_PROGRAM([], [dnl case application:start(ssl) of ok -> ok; Err -> halt(1) end, halt(0)])], [erlang_cv_ssl_runnable=yes ERLANG_APPLICATIONS="kernel,stdlib,ssl"], [ AC_RUN_IFELSE( [AC_LANG_PROGRAM([], [dnl application:start(crypto), application:start(asn1), application:start(public_key), case application:start(ssl) of ok -> ok; Err -> halt(1) end, halt(0)])], [erlang_cv_ssl_runnable=yes ERLANG_APPLICATIONS="kernel,stdlib,asn1,crypto,public_key,ssl"], [ERLANG_APPLICATIONS="kernel,stdlib" AC_MSG_RESULT(WARNING: ssl application is not working properly !!!)]) ]) ]) dnl check if crypto is working AC_CACHE_CHECK([if Erlang/OTP crypto application is running fine], [erlang_cv_crypto_runnable], [erlang_cv_crypto_runnable=no AC_RUN_IFELSE( [AC_LANG_PROGRAM([], [dnl case application:start(crypto) of ok -> case catch crypto:hash(md5, "toto") of <<247,29,190,82,98,138,63,131,167,122,180,148,129,117,37, 198>> -> ok; _ -> halt(1) end; Err -> erlang:display([Err]), halt(1) end, halt(0) ])], [ erlang_cv_crypto_runnable=yes ERLANG_APPLICATIONS="$ERLANG_APPLICATIONS,crypto" ], [ AC_MSG_RESULT([WARNING: crypto application is not working properly !!!])]) ]) dnl check if new time API is available (R18 and up) AC_CACHE_CHECK([new time API], [erlang_cv_new_time_api], [erlang_cv_new_time_api=no AC_RUN_IFELSE( [AC_LANG_PROGRAM([], [dnl R=case catch erlang:timestamp() of {A,B,C} -> 0; _ -> 1 end, halt(R)])], [erlang_cv_new_time_api=yes], [AC_MSG_RESULT(WARNING: new time API not available. use old now() instead)]) ]) AC_SUBST(erlang_cv_new_time_api) AC_SUBST(ERL_OPTS) AC_SUBST(ERLANG_APPLICATIONS) AC_SUBST(DTD,[tsung-1.0.dtd]) AC_SUBST(TEMPLATES_SUBDIR,[tsung/templates]) AC_PROG_MAKE_SET AC_PROG_INSTALL AS_AC_EXPAND(EXPANDED_LIBDIR, "$libdir") AC_MSG_NOTICE(Storing library files in $EXPANDED_LIBDIR) AS_AC_EXPAND(EXPANDED_SHAREDIR, "$datadir/tsung") AC_MSG_NOTICE(Storing data files in $EXPANDED_SHAREDIR) AC_CONFIG_FILES([\ Makefile \ tsung.spec \ tsung.sh \ tsung-recorder.sh \ examples/*.xml \ src/tsung_stats.pl \ src/tsung-plotter/tsplot.py \ src/log2tsung.pl \ src/tsung_controller/tsung_controller.app \ src/tsung_recorder/tsung_recorder.app \ src/tsung/tsung.app \ ]) AC_OUTPUT tsung-1.8.0/configure0000755000201100017670000037043114377756767014321 0ustar nniclausdream#! /bin/sh # Guess values for system-dependent variables and create Makefiles. # Generated by GNU Autoconf 2.71 for tsung 1.8.0. # # Report bugs to . # # # Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation, # Inc. # # # This configure script is free software; the Free Software Foundation # gives unlimited permission to copy, distribute and modify it. # # Copyright (C) 2008 Nicolas Niclausse ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh as_nop=: if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else $as_nop case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi # Reset variables that may have inherited troublesome values from # the environment. # IFS needs to be set, to space, tab, and newline, in precisely that order. # (If _AS_PATH_WALK were called with IFS unset, it would have the # side effect of setting IFS to empty, thus disabling word splitting.) # Quoting is to prevent editors from complaining about space-tab. as_nl=' ' export as_nl IFS=" "" $as_nl" PS1='$ ' PS2='> ' PS4='+ ' # Ensure predictable behavior from utilities with locale-dependent output. LC_ALL=C export LC_ALL LANGUAGE=C export LANGUAGE # We cannot yet rely on "unset" to work, but we need these variables # to be unset--not just set to an empty or harmless value--now, to # avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct # also avoids known problems related to "unset" and subshell syntax # in other old shells (e.g. bash 2.01 and pdksh 5.2.14). for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH do eval test \${$as_var+y} \ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : done # Ensure that fds 0, 1, and 2 are open. if (exec 3>&0) 2>/dev/null; then :; else exec 0&1) 2>/dev/null; then :; else exec 1>/dev/null; fi if (exec 3>&2) ; then :; else exec 2>/dev/null; fi # The user is always right. if ${PATH_SEPARATOR+false} :; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || PATH_SEPARATOR=';' } fi # Find who we are. Look in the path if we contain no directory separator. as_myself= case $0 in #(( *[\\/]* ) as_myself=$0 ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac test -r "$as_dir$0" && as_myself=$as_dir$0 && break done IFS=$as_save_IFS ;; esac # We did not find ourselves, most probably we were run as `sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi # Use a proper internal environment variable to ensure we don't fall # into an infinite loop, continuously re-executing ourselves. if test x"${_as_can_reexec}" != xno && test "x$CONFIG_SHELL" != x; then _as_can_reexec=no; export _as_can_reexec; # We cannot yet assume a decent shell, so we have to provide a # neutralization value for shells without unset; and this also # works around shells that cannot unset nonexistent variables. # Preserve -v and -x to the replacement shell. BASH_ENV=/dev/null ENV=/dev/null (unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV case $- in # (((( *v*x* | *x*v* ) as_opts=-vx ;; *v* ) as_opts=-v ;; *x* ) as_opts=-x ;; * ) as_opts= ;; esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed `exec'. printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi # We don't want this to propagate to other subprocesses. { _as_can_reexec=; unset _as_can_reexec;} if test "x$CONFIG_SHELL" = x; then as_bourne_compatible="as_nop=: if test \${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on \${1+\"\$@\"}, which # is contrary to our usage. Disable this feature. alias -g '\${1+\"\$@\"}'='\"\$@\"' setopt NO_GLOB_SUBST else \$as_nop case \`(set -o) 2>/dev/null\` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi " as_required="as_fn_return () { (exit \$1); } as_fn_success () { as_fn_return 0; } as_fn_failure () { as_fn_return 1; } as_fn_ret_success () { return 0; } as_fn_ret_failure () { return 1; } exitcode=0 as_fn_success || { exitcode=1; echo as_fn_success failed.; } as_fn_failure && { exitcode=1; echo as_fn_failure succeeded.; } as_fn_ret_success || { exitcode=1; echo as_fn_ret_success failed.; } as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; } if ( set x; as_fn_ret_success y && test x = \"\$1\" ) then : else \$as_nop exitcode=1; echo positional parameters were not saved. fi test x\$exitcode = x0 || exit 1 blah=\$(echo \$(echo blah)) test x\"\$blah\" = xblah || exit 1 test -x / || exit 1" as_suggested=" as_lineno_1=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_1a=\$LINENO as_lineno_2=";as_suggested=$as_suggested$LINENO;as_suggested=$as_suggested" as_lineno_2a=\$LINENO eval 'test \"x\$as_lineno_1'\$as_run'\" != \"x\$as_lineno_2'\$as_run'\" && test \"x\`expr \$as_lineno_1'\$as_run' + 1\`\" = \"x\$as_lineno_2'\$as_run'\"' || exit 1" if (eval "$as_required") 2>/dev/null then : as_have_required=yes else $as_nop as_have_required=no fi if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null then : else $as_nop as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_found=false for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac as_found=: case $as_dir in #( /*) for as_base in sh bash ksh sh5; do # Try only shells that exist, to save several forks. as_shell=$as_dir$as_base if { test -f "$as_shell" || test -f "$as_shell.exe"; } && as_run=a "$as_shell" -c "$as_bourne_compatible""$as_required" 2>/dev/null then : CONFIG_SHELL=$as_shell as_have_required=yes if as_run=a "$as_shell" -c "$as_bourne_compatible""$as_suggested" 2>/dev/null then : break 2 fi fi done;; esac as_found=false done IFS=$as_save_IFS if $as_found then : else $as_nop if { test -f "$SHELL" || test -f "$SHELL.exe"; } && as_run=a "$SHELL" -c "$as_bourne_compatible""$as_required" 2>/dev/null then : CONFIG_SHELL=$SHELL as_have_required=yes fi fi if test "x$CONFIG_SHELL" != x then : export CONFIG_SHELL # We cannot yet assume a decent shell, so we have to provide a # neutralization value for shells without unset; and this also # works around shells that cannot unset nonexistent variables. # Preserve -v and -x to the replacement shell. BASH_ENV=/dev/null ENV=/dev/null (unset BASH_ENV) >/dev/null 2>&1 && unset BASH_ENV ENV case $- in # (((( *v*x* | *x*v* ) as_opts=-vx ;; *v* ) as_opts=-v ;; *x* ) as_opts=-x ;; * ) as_opts= ;; esac exec $CONFIG_SHELL $as_opts "$as_myself" ${1+"$@"} # Admittedly, this is quite paranoid, since all the known shells bail # out after a failed `exec'. printf "%s\n" "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi if test x$as_have_required = xno then : printf "%s\n" "$0: This script requires a shell more modern than all" printf "%s\n" "$0: the shells that I found on your system." if test ${ZSH_VERSION+y} ; then printf "%s\n" "$0: In particular, zsh $ZSH_VERSION has bugs and should" printf "%s\n" "$0: be upgraded to zsh 4.3.4 or later." else printf "%s\n" "$0: Please tell bug-autoconf@gnu.org and $0: tsung-users@process-one.net about your system, $0: including any error possibly output before this $0: message. Then install a modern shell, or manually run $0: the script under such a shell if you do have one." fi exit 1 fi fi fi SHELL=${CONFIG_SHELL-/bin/sh} export SHELL # Unset more variables known to interfere with behavior of common tools. CLICOLOR_FORCE= GREP_OPTIONS= unset CLICOLOR_FORCE GREP_OPTIONS ## --------------------- ## ## M4sh Shell Functions. ## ## --------------------- ## # as_fn_unset VAR # --------------- # Portably unset VAR. as_fn_unset () { { eval $1=; unset $1;} } as_unset=as_fn_unset # as_fn_set_status STATUS # ----------------------- # Set $? to STATUS, without forking. as_fn_set_status () { return $1 } # as_fn_set_status # as_fn_exit STATUS # ----------------- # Exit the shell with STATUS, even in a "trap 0" or "set -e" context. as_fn_exit () { set +e as_fn_set_status $1 exit $1 } # as_fn_exit # as_fn_nop # --------- # Do nothing but, unlike ":", preserve the value of $?. as_fn_nop () { return $? } as_nop=as_fn_nop # as_fn_mkdir_p # ------------- # Create "$as_dir" as a directory, including parents if necessary. as_fn_mkdir_p () { case $as_dir in #( -*) as_dir=./$as_dir;; esac test -d "$as_dir" || eval $as_mkdir_p || { as_dirs= while :; do case $as_dir in #( *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" as_dir=`$as_dirname -- "$as_dir" || $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` test -d "$as_dir" && break done test -z "$as_dirs" || eval "mkdir $as_dirs" } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" } # as_fn_mkdir_p # as_fn_executable_p FILE # ----------------------- # Test if FILE is an executable regular file. as_fn_executable_p () { test -f "$1" && test -x "$1" } # as_fn_executable_p # as_fn_append VAR VALUE # ---------------------- # Append the text in VALUE to the end of the definition contained in VAR. Take # advantage of any shell optimizations that allow amortized linear growth over # repeated appends, instead of the typical quadratic growth present in naive # implementations. if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null then : eval 'as_fn_append () { eval $1+=\$2 }' else $as_nop as_fn_append () { eval $1=\$$1\$2 } fi # as_fn_append # as_fn_arith ARG... # ------------------ # Perform arithmetic evaluation on the ARGs, and store the result in the # global $as_val. Take advantage of shells that can avoid forks. The arguments # must be portable across $(()) and expr. if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null then : eval 'as_fn_arith () { as_val=$(( $* )) }' else $as_nop as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` } fi # as_fn_arith # as_fn_nop # --------- # Do nothing but, unlike ":", preserve the value of $?. as_fn_nop () { return $? } as_nop=as_fn_nop # as_fn_error STATUS ERROR [LINENO LOG_FD] # ---------------------------------------- # Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are # provided, also output the error to LOG_FD, referencing LINENO. Then exit the # script with STATUS, using 1 if that was 0. as_fn_error () { as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi printf "%s\n" "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error if expr a : '\(a\)' >/dev/null 2>&1 && test "X`expr 00001 : '.*\(...\)'`" = X001; then as_expr=expr else as_expr=false fi if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then as_basename=basename else as_basename=false fi if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then as_dirname=dirname else as_dirname=false fi as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q } /^X\/\(\/\/\)$/{ s//\1/ q } /^X\/\(\/\).*/{ s//\1/ q } s/.*/./; q'` # Avoid depending upon Character Ranges. as_cr_letters='abcdefghijklmnopqrstuvwxyz' as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits as_lineno_1=$LINENO as_lineno_1a=$LINENO as_lineno_2=$LINENO as_lineno_2a=$LINENO eval 'test "x$as_lineno_1'$as_run'" != "x$as_lineno_2'$as_run'" && test "x`expr $as_lineno_1'$as_run' + 1`" = "x$as_lineno_2'$as_run'"' || { # Blame Lee E. McMahon (1931-1989) for sed's syntax. :-) sed -n ' p /[$]LINENO/= ' <$as_myself | sed ' s/[$]LINENO.*/&-/ t lineno b :lineno N :loop s/[$]LINENO\([^'$as_cr_alnum'_].*\n\)\(.*\)/\2\1\2/ t loop s/-\n.*// ' >$as_me.lineno && chmod +x "$as_me.lineno" || { printf "%s\n" "$as_me: error: cannot create $as_me.lineno; rerun with a POSIX shell" >&2; as_fn_exit 1; } # If we had to re-execute with $CONFIG_SHELL, we're ensured to have # already done that, so ensure we don't try to do so again and fall # in an infinite loop. This has already happened in practice. _as_can_reexec=no; export _as_can_reexec # Don't try to exec as it changes $[0], causing all sort of problems # (the dirname of $[0] is not the place where we might find the # original and so on. Autoconf is especially sensitive to this). . "./$as_me.lineno" # Exit status is that of the last command. exit } # Determine whether it's possible to make 'echo' print without a newline. # These variables are no longer used directly by Autoconf, but are AC_SUBSTed # for compatibility with existing Makefiles. ECHO_C= ECHO_N= ECHO_T= case `echo -n x` in #((((( -n*) case `echo 'xy\c'` in *c*) ECHO_T=' ';; # ECHO_T is single tab character. xy) ECHO_C='\c';; *) echo `echo ksh88 bug on AIX 6.1` > /dev/null ECHO_T=' ';; esac;; *) ECHO_N='-n';; esac # For backward compatibility with old third-party macros, we provide # the shell variables $as_echo and $as_echo_n. New code should use # AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. as_echo='printf %s\n' as_echo_n='printf %s' rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file else rm -f conf$$.dir mkdir conf$$.dir 2>/dev/null fi if (echo >conf$$.file) 2>/dev/null; then if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. # In both cases, we have to default to `cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then as_ln_s=ln else as_ln_s='cp -pR' fi else as_ln_s='cp -pR' fi rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file rmdir conf$$.dir 2>/dev/null if mkdir -p . 2>/dev/null; then as_mkdir_p='mkdir -p "$as_dir"' else test -d ./-p && rmdir ./-p as_mkdir_p=false fi as_test_x='test -x' as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" # Sed expression to map a string onto a valid variable name. as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" test -n "$DJDIR" || exec 7<&0 &1 # Name of the host. # hostname on some systems (SVR3.2, old GNU/Linux) returns a bogus exit status, # so uname gets run too. ac_hostname=`(hostname || uname -n) 2>/dev/null | sed 1q` # # Initializations. # ac_default_prefix=/usr/local ac_clean_files= ac_config_libobj_dir=. LIBOBJS= cross_compiling=no subdirs= MFLAGS= MAKEFLAGS= # Identity of this package. PACKAGE_NAME='tsung' PACKAGE_TARNAME='tsung' PACKAGE_VERSION='1.8.0' PACKAGE_STRING='tsung 1.8.0' PACKAGE_BUGREPORT='tsung-users@process-one.net' PACKAGE_URL='' ac_unique_file="src/tsung/tsung.erl" ac_subst_vars='LTLIBOBJS LIBOBJS EXPANDED_SHAREDIR EXPANDED_LIBDIR INSTALL_DATA INSTALL_SCRIPT INSTALL_PROGRAM SET_MAKE TEMPLATES_SUBDIR DTD ERLANG_APPLICATIONS ERL_OPTS erlang_cv_new_time_api ERLANG_LIB_VER_public_key ERLANG_LIB_DIR_public_key ERLANG_LIB_VER_crypto ERLANG_LIB_DIR_crypto ERLANG_LIB_VER_ssl ERLANG_LIB_DIR_ssl ERLANG_LIB_VER_xmerl ERLANG_LIB_DIR_xmerl ERLANG_ROOT_DIR ac_prefix_program DIALYZER ERL ERLCFLAGS ERLC SED CONFIGURE_DEPENDENCIES CONFIG_STATUS_DEPENDENCIES target_alias host_alias build_alias LIBS ECHO_T ECHO_N ECHO_C DEFS mandir localedir libdir psdir pdfdir dvidir htmldir infodir docdir oldincludedir includedir runstatedir localstatedir sharedstatedir sysconfdir datadir datarootdir libexecdir sbindir bindir program_transform_name prefix exec_prefix PACKAGE_URL PACKAGE_BUGREPORT PACKAGE_STRING PACKAGE_VERSION PACKAGE_TARNAME PACKAGE_NAME PATH_SEPARATOR SHELL' ac_subst_files='' ac_user_opts=' enable_option_checking with_erlang ' ac_precious_vars='build_alias host_alias target_alias ERLC ERLCFLAGS ERL' # Initialize some variables set by options. ac_init_help= ac_init_version=false ac_unrecognized_opts= ac_unrecognized_sep= # The variables have the same names as the options, with # dashes changed to underlines. cache_file=/dev/null exec_prefix=NONE no_create= no_recursion= prefix=NONE program_prefix=NONE program_suffix=NONE program_transform_name=s,x,x, silent= site= srcdir= verbose= x_includes=NONE x_libraries=NONE # Installation directory options. # These are left unexpanded so users can "make install exec_prefix=/foo" # and all the variables that are supposed to be based on exec_prefix # by default will actually change. # Use braces instead of parens because sh, perl, etc. also accept them. # (The list follows the same order as the GNU Coding Standards.) bindir='${exec_prefix}/bin' sbindir='${exec_prefix}/sbin' libexecdir='${exec_prefix}/libexec' datarootdir='${prefix}/share' datadir='${datarootdir}' sysconfdir='${prefix}/etc' sharedstatedir='${prefix}/com' localstatedir='${prefix}/var' runstatedir='${localstatedir}/run' includedir='${prefix}/include' oldincludedir='/usr/include' docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' infodir='${datarootdir}/info' htmldir='${docdir}' dvidir='${docdir}' pdfdir='${docdir}' psdir='${docdir}' libdir='${exec_prefix}/lib' localedir='${datarootdir}/locale' mandir='${datarootdir}/man' ac_prev= ac_dashdash= for ac_option do # If the previous option needs an argument, assign it. if test -n "$ac_prev"; then eval $ac_prev=\$ac_option ac_prev= continue fi case $ac_option in *=?*) ac_optarg=`expr "X$ac_option" : '[^=]*=\(.*\)'` ;; *=) ac_optarg= ;; *) ac_optarg=yes ;; esac case $ac_dashdash$ac_option in --) ac_dashdash=yes ;; -bindir | --bindir | --bindi | --bind | --bin | --bi) ac_prev=bindir ;; -bindir=* | --bindir=* | --bindi=* | --bind=* | --bin=* | --bi=*) bindir=$ac_optarg ;; -build | --build | --buil | --bui | --bu) ac_prev=build_alias ;; -build=* | --build=* | --buil=* | --bui=* | --bu=*) build_alias=$ac_optarg ;; -cache-file | --cache-file | --cache-fil | --cache-fi \ | --cache-f | --cache- | --cache | --cach | --cac | --ca | --c) ac_prev=cache_file ;; -cache-file=* | --cache-file=* | --cache-fil=* | --cache-fi=* \ | --cache-f=* | --cache-=* | --cache=* | --cach=* | --cac=* | --ca=* | --c=*) cache_file=$ac_optarg ;; --config-cache | -C) cache_file=config.cache ;; -datadir | --datadir | --datadi | --datad) ac_prev=datadir ;; -datadir=* | --datadir=* | --datadi=* | --datad=*) datadir=$ac_optarg ;; -datarootdir | --datarootdir | --datarootdi | --datarootd | --dataroot \ | --dataroo | --dataro | --datar) ac_prev=datarootdir ;; -datarootdir=* | --datarootdir=* | --datarootdi=* | --datarootd=* \ | --dataroot=* | --dataroo=* | --dataro=* | --datar=*) datarootdir=$ac_optarg ;; -disable-* | --disable-*) ac_useropt=`expr "x$ac_option" : 'x-*disable-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: \`$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--disable-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval enable_$ac_useropt=no ;; -docdir | --docdir | --docdi | --doc | --do) ac_prev=docdir ;; -docdir=* | --docdir=* | --docdi=* | --doc=* | --do=*) docdir=$ac_optarg ;; -dvidir | --dvidir | --dvidi | --dvid | --dvi | --dv) ac_prev=dvidir ;; -dvidir=* | --dvidir=* | --dvidi=* | --dvid=* | --dvi=* | --dv=*) dvidir=$ac_optarg ;; -enable-* | --enable-*) ac_useropt=`expr "x$ac_option" : 'x-*enable-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid feature name: \`$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "enable_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--enable-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval enable_$ac_useropt=\$ac_optarg ;; -exec-prefix | --exec_prefix | --exec-prefix | --exec-prefi \ | --exec-pref | --exec-pre | --exec-pr | --exec-p | --exec- \ | --exec | --exe | --ex) ac_prev=exec_prefix ;; -exec-prefix=* | --exec_prefix=* | --exec-prefix=* | --exec-prefi=* \ | --exec-pref=* | --exec-pre=* | --exec-pr=* | --exec-p=* | --exec-=* \ | --exec=* | --exe=* | --ex=*) exec_prefix=$ac_optarg ;; -gas | --gas | --ga | --g) # Obsolete; use --with-gas. with_gas=yes ;; -help | --help | --hel | --he | -h) ac_init_help=long ;; -help=r* | --help=r* | --hel=r* | --he=r* | -hr*) ac_init_help=recursive ;; -help=s* | --help=s* | --hel=s* | --he=s* | -hs*) ac_init_help=short ;; -host | --host | --hos | --ho) ac_prev=host_alias ;; -host=* | --host=* | --hos=* | --ho=*) host_alias=$ac_optarg ;; -htmldir | --htmldir | --htmldi | --htmld | --html | --htm | --ht) ac_prev=htmldir ;; -htmldir=* | --htmldir=* | --htmldi=* | --htmld=* | --html=* | --htm=* \ | --ht=*) htmldir=$ac_optarg ;; -includedir | --includedir | --includedi | --included | --include \ | --includ | --inclu | --incl | --inc) ac_prev=includedir ;; -includedir=* | --includedir=* | --includedi=* | --included=* | --include=* \ | --includ=* | --inclu=* | --incl=* | --inc=*) includedir=$ac_optarg ;; -infodir | --infodir | --infodi | --infod | --info | --inf) ac_prev=infodir ;; -infodir=* | --infodir=* | --infodi=* | --infod=* | --info=* | --inf=*) infodir=$ac_optarg ;; -libdir | --libdir | --libdi | --libd) ac_prev=libdir ;; -libdir=* | --libdir=* | --libdi=* | --libd=*) libdir=$ac_optarg ;; -libexecdir | --libexecdir | --libexecdi | --libexecd | --libexec \ | --libexe | --libex | --libe) ac_prev=libexecdir ;; -libexecdir=* | --libexecdir=* | --libexecdi=* | --libexecd=* | --libexec=* \ | --libexe=* | --libex=* | --libe=*) libexecdir=$ac_optarg ;; -localedir | --localedir | --localedi | --localed | --locale) ac_prev=localedir ;; -localedir=* | --localedir=* | --localedi=* | --localed=* | --locale=*) localedir=$ac_optarg ;; -localstatedir | --localstatedir | --localstatedi | --localstated \ | --localstate | --localstat | --localsta | --localst | --locals) ac_prev=localstatedir ;; -localstatedir=* | --localstatedir=* | --localstatedi=* | --localstated=* \ | --localstate=* | --localstat=* | --localsta=* | --localst=* | --locals=*) localstatedir=$ac_optarg ;; -mandir | --mandir | --mandi | --mand | --man | --ma | --m) ac_prev=mandir ;; -mandir=* | --mandir=* | --mandi=* | --mand=* | --man=* | --ma=* | --m=*) mandir=$ac_optarg ;; -nfp | --nfp | --nf) # Obsolete; use --without-fp. with_fp=no ;; -no-create | --no-create | --no-creat | --no-crea | --no-cre \ | --no-cr | --no-c | -n) no_create=yes ;; -no-recursion | --no-recursion | --no-recursio | --no-recursi \ | --no-recurs | --no-recur | --no-recu | --no-rec | --no-re | --no-r) no_recursion=yes ;; -oldincludedir | --oldincludedir | --oldincludedi | --oldincluded \ | --oldinclude | --oldinclud | --oldinclu | --oldincl | --oldinc \ | --oldin | --oldi | --old | --ol | --o) ac_prev=oldincludedir ;; -oldincludedir=* | --oldincludedir=* | --oldincludedi=* | --oldincluded=* \ | --oldinclude=* | --oldinclud=* | --oldinclu=* | --oldincl=* | --oldinc=* \ | --oldin=* | --oldi=* | --old=* | --ol=* | --o=*) oldincludedir=$ac_optarg ;; -prefix | --prefix | --prefi | --pref | --pre | --pr | --p) ac_prev=prefix ;; -prefix=* | --prefix=* | --prefi=* | --pref=* | --pre=* | --pr=* | --p=*) prefix=$ac_optarg ;; -program-prefix | --program-prefix | --program-prefi | --program-pref \ | --program-pre | --program-pr | --program-p) ac_prev=program_prefix ;; -program-prefix=* | --program-prefix=* | --program-prefi=* \ | --program-pref=* | --program-pre=* | --program-pr=* | --program-p=*) program_prefix=$ac_optarg ;; -program-suffix | --program-suffix | --program-suffi | --program-suff \ | --program-suf | --program-su | --program-s) ac_prev=program_suffix ;; -program-suffix=* | --program-suffix=* | --program-suffi=* \ | --program-suff=* | --program-suf=* | --program-su=* | --program-s=*) program_suffix=$ac_optarg ;; -program-transform-name | --program-transform-name \ | --program-transform-nam | --program-transform-na \ | --program-transform-n | --program-transform- \ | --program-transform | --program-transfor \ | --program-transfo | --program-transf \ | --program-trans | --program-tran \ | --progr-tra | --program-tr | --program-t) ac_prev=program_transform_name ;; -program-transform-name=* | --program-transform-name=* \ | --program-transform-nam=* | --program-transform-na=* \ | --program-transform-n=* | --program-transform-=* \ | --program-transform=* | --program-transfor=* \ | --program-transfo=* | --program-transf=* \ | --program-trans=* | --program-tran=* \ | --progr-tra=* | --program-tr=* | --program-t=*) program_transform_name=$ac_optarg ;; -pdfdir | --pdfdir | --pdfdi | --pdfd | --pdf | --pd) ac_prev=pdfdir ;; -pdfdir=* | --pdfdir=* | --pdfdi=* | --pdfd=* | --pdf=* | --pd=*) pdfdir=$ac_optarg ;; -psdir | --psdir | --psdi | --psd | --ps) ac_prev=psdir ;; -psdir=* | --psdir=* | --psdi=* | --psd=* | --ps=*) psdir=$ac_optarg ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil) silent=yes ;; -runstatedir | --runstatedir | --runstatedi | --runstated \ | --runstate | --runstat | --runsta | --runst | --runs \ | --run | --ru | --r) ac_prev=runstatedir ;; -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ | --run=* | --ru=* | --r=*) runstatedir=$ac_optarg ;; -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) ac_prev=sbindir ;; -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ | --sbi=* | --sb=*) sbindir=$ac_optarg ;; -sharedstatedir | --sharedstatedir | --sharedstatedi \ | --sharedstated | --sharedstate | --sharedstat | --sharedsta \ | --sharedst | --shareds | --shared | --share | --shar \ | --sha | --sh) ac_prev=sharedstatedir ;; -sharedstatedir=* | --sharedstatedir=* | --sharedstatedi=* \ | --sharedstated=* | --sharedstate=* | --sharedstat=* | --sharedsta=* \ | --sharedst=* | --shareds=* | --shared=* | --share=* | --shar=* \ | --sha=* | --sh=*) sharedstatedir=$ac_optarg ;; -site | --site | --sit) ac_prev=site ;; -site=* | --site=* | --sit=*) site=$ac_optarg ;; -srcdir | --srcdir | --srcdi | --srcd | --src | --sr) ac_prev=srcdir ;; -srcdir=* | --srcdir=* | --srcdi=* | --srcd=* | --src=* | --sr=*) srcdir=$ac_optarg ;; -sysconfdir | --sysconfdir | --sysconfdi | --sysconfd | --sysconf \ | --syscon | --sysco | --sysc | --sys | --sy) ac_prev=sysconfdir ;; -sysconfdir=* | --sysconfdir=* | --sysconfdi=* | --sysconfd=* | --sysconf=* \ | --syscon=* | --sysco=* | --sysc=* | --sys=* | --sy=*) sysconfdir=$ac_optarg ;; -target | --target | --targe | --targ | --tar | --ta | --t) ac_prev=target_alias ;; -target=* | --target=* | --targe=* | --targ=* | --tar=* | --ta=* | --t=*) target_alias=$ac_optarg ;; -v | -verbose | --verbose | --verbos | --verbo | --verb) verbose=yes ;; -version | --version | --versio | --versi | --vers | -V) ac_init_version=: ;; -with-* | --with-*) ac_useropt=`expr "x$ac_option" : 'x-*with-\([^=]*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: \`$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--with-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval with_$ac_useropt=\$ac_optarg ;; -without-* | --without-*) ac_useropt=`expr "x$ac_option" : 'x-*without-\(.*\)'` # Reject names that are not valid shell variable names. expr "x$ac_useropt" : ".*[^-+._$as_cr_alnum]" >/dev/null && as_fn_error $? "invalid package name: \`$ac_useropt'" ac_useropt_orig=$ac_useropt ac_useropt=`printf "%s\n" "$ac_useropt" | sed 's/[-+.]/_/g'` case $ac_user_opts in *" "with_$ac_useropt" "*) ;; *) ac_unrecognized_opts="$ac_unrecognized_opts$ac_unrecognized_sep--without-$ac_useropt_orig" ac_unrecognized_sep=', ';; esac eval with_$ac_useropt=no ;; --x) # Obsolete; use --with-x. with_x=yes ;; -x-includes | --x-includes | --x-include | --x-includ | --x-inclu \ | --x-incl | --x-inc | --x-in | --x-i) ac_prev=x_includes ;; -x-includes=* | --x-includes=* | --x-include=* | --x-includ=* | --x-inclu=* \ | --x-incl=* | --x-inc=* | --x-in=* | --x-i=*) x_includes=$ac_optarg ;; -x-libraries | --x-libraries | --x-librarie | --x-librari \ | --x-librar | --x-libra | --x-libr | --x-lib | --x-li | --x-l) ac_prev=x_libraries ;; -x-libraries=* | --x-libraries=* | --x-librarie=* | --x-librari=* \ | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*) x_libraries=$ac_optarg ;; -*) as_fn_error $? "unrecognized option: \`$ac_option' Try \`$0 --help' for more information" ;; *=*) ac_envvar=`expr "x$ac_option" : 'x\([^=]*\)='` # Reject names that are not valid shell variable names. case $ac_envvar in #( '' | [0-9]* | *[!_$as_cr_alnum]* ) as_fn_error $? "invalid variable name: \`$ac_envvar'" ;; esac eval $ac_envvar=\$ac_optarg export $ac_envvar ;; *) # FIXME: should be removed in autoconf 3.0. printf "%s\n" "$as_me: WARNING: you should use --build, --host, --target" >&2 expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && printf "%s\n" "$as_me: WARNING: invalid host type: $ac_option" >&2 : "${build_alias=$ac_option} ${host_alias=$ac_option} ${target_alias=$ac_option}" ;; esac done if test -n "$ac_prev"; then ac_option=--`echo $ac_prev | sed 's/_/-/g'` as_fn_error $? "missing argument to $ac_option" fi if test -n "$ac_unrecognized_opts"; then case $enable_option_checking in no) ;; fatal) as_fn_error $? "unrecognized options: $ac_unrecognized_opts" ;; *) printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2 ;; esac fi # Check all directory arguments for consistency. for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ datadir sysconfdir sharedstatedir localstatedir includedir \ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ libdir localedir mandir runstatedir do eval ac_val=\$$ac_var # Remove trailing slashes. case $ac_val in */ ) ac_val=`expr "X$ac_val" : 'X\(.*[^/]\)' \| "X$ac_val" : 'X\(.*\)'` eval $ac_var=\$ac_val;; esac # Be sure to have absolute directory names. case $ac_val in [\\/$]* | ?:[\\/]* ) continue;; NONE | '' ) case $ac_var in *prefix ) continue;; esac;; esac as_fn_error $? "expected an absolute directory name for --$ac_var: $ac_val" done # There might be people who depend on the old broken behavior: `$host' # used to hold the argument of --host etc. # FIXME: To remove some day. build=$build_alias host=$host_alias target=$target_alias # FIXME: To remove some day. if test "x$host_alias" != x; then if test "x$build_alias" = x; then cross_compiling=maybe elif test "x$build_alias" != "x$host_alias"; then cross_compiling=yes fi fi ac_tool_prefix= test -n "$host_alias" && ac_tool_prefix=$host_alias- test "$silent" = yes && exec 6>/dev/null ac_pwd=`pwd` && test -n "$ac_pwd" && ac_ls_di=`ls -di .` && ac_pwd_ls_di=`cd "$ac_pwd" && ls -di .` || as_fn_error $? "working directory cannot be determined" test "X$ac_ls_di" = "X$ac_pwd_ls_di" || as_fn_error $? "pwd does not report name of working directory" # Find the source files, if location was not specified. if test -z "$srcdir"; then ac_srcdir_defaulted=yes # Try the directory containing this script, then the parent directory. ac_confdir=`$as_dirname -- "$as_myself" || $as_expr X"$as_myself" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_myself" : 'X\(//\)[^/]' \| \ X"$as_myself" : 'X\(//\)$' \| \ X"$as_myself" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X"$as_myself" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` srcdir=$ac_confdir if test ! -r "$srcdir/$ac_unique_file"; then srcdir=.. fi else ac_srcdir_defaulted=no fi if test ! -r "$srcdir/$ac_unique_file"; then test "$ac_srcdir_defaulted" = yes && srcdir="$ac_confdir or .." as_fn_error $? "cannot find sources ($ac_unique_file) in $srcdir" fi ac_msg="sources are in $srcdir, but \`cd $srcdir' does not work" ac_abs_confdir=`( cd "$srcdir" && test -r "./$ac_unique_file" || as_fn_error $? "$ac_msg" pwd)` # When building in place, set srcdir=. if test "$ac_abs_confdir" = "$ac_pwd"; then srcdir=. fi # Remove unnecessary trailing slashes from srcdir. # Double slashes in file names in object file debugging info # mess up M-x gdb in Emacs. case $srcdir in */) srcdir=`expr "X$srcdir" : 'X\(.*[^/]\)' \| "X$srcdir" : 'X\(.*\)'`;; esac for ac_var in $ac_precious_vars; do eval ac_env_${ac_var}_set=\${${ac_var}+set} eval ac_env_${ac_var}_value=\$${ac_var} eval ac_cv_env_${ac_var}_set=\${${ac_var}+set} eval ac_cv_env_${ac_var}_value=\$${ac_var} done # # Report the --help message. # if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF \`configure' configures tsung 1.8.0 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... To assign environment variables (e.g., CC, CFLAGS...), specify them as VAR=VALUE. See below for descriptions of some of the useful variables. Defaults for the options are specified in brackets. Configuration: -h, --help display this help and exit --help=short display options specific to this package --help=recursive display the short help of all the included packages -V, --version display version information and exit -q, --quiet, --silent do not print \`checking ...' messages --cache-file=FILE cache test results in FILE [disabled] -C, --config-cache alias for \`--cache-file=config.cache' -n, --no-create do not create output files --srcdir=DIR find the sources in DIR [configure dir or \`..'] Installation directories: --prefix=PREFIX install architecture-independent files in PREFIX [$ac_default_prefix] --exec-prefix=EPREFIX install architecture-dependent files in EPREFIX [PREFIX] By default, \`make install' will install all the files in \`$ac_default_prefix/bin', \`$ac_default_prefix/lib' etc. You can specify an installation prefix other than \`$ac_default_prefix' using \`--prefix', for instance \`--prefix=\$HOME'. For better control, use the options below. Fine tuning of the installation directories: --bindir=DIR user executables [EPREFIX/bin] --sbindir=DIR system admin executables [EPREFIX/sbin] --libexecdir=DIR program executables [EPREFIX/libexec] --sysconfdir=DIR read-only single-machine data [PREFIX/etc] --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] --localstatedir=DIR modifiable single-machine data [PREFIX/var] --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] --libdir=DIR object code libraries [EPREFIX/lib] --includedir=DIR C header files [PREFIX/include] --oldincludedir=DIR C header files for non-gcc [/usr/include] --datarootdir=DIR read-only arch.-independent data root [PREFIX/share] --datadir=DIR read-only architecture-independent data [DATAROOTDIR] --infodir=DIR info documentation [DATAROOTDIR/info] --localedir=DIR locale-dependent data [DATAROOTDIR/locale] --mandir=DIR man documentation [DATAROOTDIR/man] --docdir=DIR documentation root [DATAROOTDIR/doc/tsung] --htmldir=DIR html documentation [DOCDIR] --dvidir=DIR dvi documentation [DOCDIR] --pdfdir=DIR pdf documentation [DOCDIR] --psdir=DIR ps documentation [DOCDIR] _ACEOF cat <<\_ACEOF _ACEOF fi if test -n "$ac_init_help"; then case $ac_init_help in short | recursive ) echo "Configuration of tsung 1.8.0:";; esac cat <<\_ACEOF Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) --with-erlang=PREFIX path to erlc and erl Some influential environment variables: ERLC Erlang/OTP compiler command [autodetected] ERLCFLAGS Erlang/OTP compiler flags [none] ERL Erlang/OTP interpreter command [autodetected] Use these variables to override the choices made by `configure' or to help it to find libraries and programs with nonstandard names/locations. Report bugs to . _ACEOF ac_status=$? fi if test "$ac_init_help" = "recursive"; then # If there are subdirs, report their specific --help. for ac_dir in : $ac_subdirs_all; do test "x$ac_dir" = x: && continue test -d "$ac_dir" || { cd "$srcdir" && ac_pwd=`pwd` && srcdir=. && test -d "$ac_dir"; } || continue ac_builddir=. case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; esac ;; esac ac_abs_top_builddir=$ac_pwd ac_abs_builddir=$ac_pwd$ac_dir_suffix # for backward compatibility: ac_top_builddir=$ac_top_build_prefix case $srcdir in .) # We are building in place. ac_srcdir=. ac_top_srcdir=$ac_top_builddir_sub ac_abs_top_srcdir=$ac_pwd ;; [\\/]* | ?:[\\/]* ) # Absolute name. ac_srcdir=$srcdir$ac_dir_suffix; ac_top_srcdir=$srcdir ac_abs_top_srcdir=$srcdir ;; *) # Relative name. ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix ac_top_srcdir=$ac_top_build_prefix$srcdir ac_abs_top_srcdir=$ac_pwd/$srcdir ;; esac ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix cd "$ac_dir" || { ac_status=$?; continue; } # Check for configure.gnu first; this name is used for a wrapper for # Metaconfig's "Configure" on case-insensitive file systems. if test -f "$ac_srcdir/configure.gnu"; then echo && $SHELL "$ac_srcdir/configure.gnu" --help=recursive elif test -f "$ac_srcdir/configure"; then echo && $SHELL "$ac_srcdir/configure" --help=recursive else printf "%s\n" "$as_me: WARNING: no configuration information is in $ac_dir" >&2 fi || ac_status=$? cd "$ac_pwd" || { ac_status=$?; break; } done fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF tsung configure 1.8.0 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. This configure script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it. Copyright (C) 2008 Nicolas Niclausse _ACEOF exit fi ## ------------------------ ## ## Autoconf initialization. ## ## ------------------------ ## # ac_fn_erl_try_run LINENO # ------------------------ # Try to run conftest.$ac_ext, and return whether this succeeded. Assumes that # executables *can* be run. ac_fn_erl_try_run () { as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack if { { ac_try="$ac_link" case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; } && { ac_try='./conftest$ac_exeext' { { case "(($ac_try" in *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; *) ac_try_echo=$ac_try;; esac eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" printf "%s\n" "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; } then : ac_retval=0 else $as_nop printf "%s\n" "$as_me: program exited with status $ac_status" >&5 printf "%s\n" "$as_me: failed program was:" >&5 sed 's/^/| /' conftest.$ac_ext >&5 ac_retval=$ac_status fi rm -rf conftest.dSYM conftest_ipa8_conftest.oo eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno as_fn_set_status $ac_retval } # ac_fn_erl_try_run ac_configure_args_raw= for ac_arg do case $ac_arg in *\'*) ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac as_fn_append ac_configure_args_raw " '$ac_arg'" done case $ac_configure_args_raw in *$as_nl*) ac_safe_unquote= ;; *) ac_unsafe_z='|&;<>()$`\\"*?[ '' ' # This string ends in space, tab. ac_unsafe_a="$ac_unsafe_z#~" ac_safe_unquote="s/ '\\([^$ac_unsafe_a][^$ac_unsafe_z]*\\)'/ \\1/g" ac_configure_args_raw=` printf "%s\n" "$ac_configure_args_raw" | sed "$ac_safe_unquote"`;; esac cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. It was created by tsung $as_me 1.8.0, which was generated by GNU Autoconf 2.71. Invocation command line was $ $0$ac_configure_args_raw _ACEOF exec 5>>config.log { cat <<_ASUNAME ## --------- ## ## Platform. ## ## --------- ## hostname = `(hostname || uname -n) 2>/dev/null | sed 1q` uname -m = `(uname -m) 2>/dev/null || echo unknown` uname -r = `(uname -r) 2>/dev/null || echo unknown` uname -s = `(uname -s) 2>/dev/null || echo unknown` uname -v = `(uname -v) 2>/dev/null || echo unknown` /usr/bin/uname -p = `(/usr/bin/uname -p) 2>/dev/null || echo unknown` /bin/uname -X = `(/bin/uname -X) 2>/dev/null || echo unknown` /bin/arch = `(/bin/arch) 2>/dev/null || echo unknown` /usr/bin/arch -k = `(/usr/bin/arch -k) 2>/dev/null || echo unknown` /usr/convex/getsysinfo = `(/usr/convex/getsysinfo) 2>/dev/null || echo unknown` /usr/bin/hostinfo = `(/usr/bin/hostinfo) 2>/dev/null || echo unknown` /bin/machine = `(/bin/machine) 2>/dev/null || echo unknown` /usr/bin/oslevel = `(/usr/bin/oslevel) 2>/dev/null || echo unknown` /bin/universe = `(/bin/universe) 2>/dev/null || echo unknown` _ASUNAME as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac printf "%s\n" "PATH: $as_dir" done IFS=$as_save_IFS } >&5 cat >&5 <<_ACEOF ## ----------- ## ## Core tests. ## ## ----------- ## _ACEOF # Keep a trace of the command line. # Strip out --no-create and --no-recursion so they do not pile up. # Strip out --silent because we don't want to record it for future runs. # Also quote any args containing shell meta-characters. # Make two passes to allow for proper duplicate-argument suppression. ac_configure_args= ac_configure_args0= ac_configure_args1= ac_must_keep_next=false for ac_pass in 1 2 do for ac_arg do case $ac_arg in -no-create | --no-c* | -n | -no-recursion | --no-r*) continue ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil) continue ;; *\'*) ac_arg=`printf "%s\n" "$ac_arg" | sed "s/'/'\\\\\\\\''/g"` ;; esac case $ac_pass in 1) as_fn_append ac_configure_args0 " '$ac_arg'" ;; 2) as_fn_append ac_configure_args1 " '$ac_arg'" if test $ac_must_keep_next = true; then ac_must_keep_next=false # Got value, back to normal. else case $ac_arg in *=* | --config-cache | -C | -disable-* | --disable-* \ | -enable-* | --enable-* | -gas | --g* | -nfp | --nf* \ | -q | -quiet | --q* | -silent | --sil* | -v | -verb* \ | -with-* | --with-* | -without-* | --without-* | --x) case "$ac_configure_args0 " in "$ac_configure_args1"*" '$ac_arg' "* ) continue ;; esac ;; -* ) ac_must_keep_next=true ;; esac fi as_fn_append ac_configure_args " '$ac_arg'" ;; esac done done { ac_configure_args0=; unset ac_configure_args0;} { ac_configure_args1=; unset ac_configure_args1;} # When interrupted or exit'd, cleanup temporary files, and complete # config.log. We remove comments because anyway the quotes in there # would cause problems or look ugly. # WARNING: Use '\'' to represent an apostrophe within the trap. # WARNING: Do not start the trap code with a newline, due to a FreeBSD 4.0 bug. trap 'exit_status=$? # Sanitize IFS. IFS=" "" $as_nl" # Save into config.log some information that might help in debugging. { echo printf "%s\n" "## ---------------- ## ## Cache variables. ## ## ---------------- ##" echo # The following way of writing the cache mishandles newlines in values, ( for ac_var in `(set) 2>&1 | sed -n '\''s/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'\''`; do eval ac_val=\$$ac_var case $ac_val in #( *${as_nl}*) case $ac_var in #( *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( *) { eval $ac_var=; unset $ac_var;} ;; esac ;; esac done (set) 2>&1 | case $as_nl`(ac_space='\'' '\''; set) 2>&1` in #( *${as_nl}ac_space=\ *) sed -n \ "s/'\''/'\''\\\\'\'''\''/g; s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\''\\2'\''/p" ;; #( *) sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | sort ) echo printf "%s\n" "## ----------------- ## ## Output variables. ## ## ----------------- ##" echo for ac_var in $ac_subst_vars do eval ac_val=\$$ac_var case $ac_val in *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac printf "%s\n" "$ac_var='\''$ac_val'\''" done | sort echo if test -n "$ac_subst_files"; then printf "%s\n" "## ------------------- ## ## File substitutions. ## ## ------------------- ##" echo for ac_var in $ac_subst_files do eval ac_val=\$$ac_var case $ac_val in *\'\''*) ac_val=`printf "%s\n" "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac printf "%s\n" "$ac_var='\''$ac_val'\''" done | sort echo fi if test -s confdefs.h; then printf "%s\n" "## ----------- ## ## confdefs.h. ## ## ----------- ##" echo cat confdefs.h echo fi test "$ac_signal" != 0 && printf "%s\n" "$as_me: caught signal $ac_signal" printf "%s\n" "$as_me: exit $exit_status" } >&5 rm -f core *.core core.conftest.* && rm -f -r conftest* confdefs* conf$$* $ac_clean_files && exit $exit_status ' 0 for ac_signal in 1 2 13 15; do trap 'ac_signal='$ac_signal'; as_fn_exit 1' $ac_signal done ac_signal=0 # confdefs.h avoids OS command line length limits that DEFS can exceed. rm -f -r conftest* confdefs.h printf "%s\n" "/* confdefs.h */" > confdefs.h # Predefined preprocessor variables. printf "%s\n" "#define PACKAGE_NAME \"$PACKAGE_NAME\"" >>confdefs.h printf "%s\n" "#define PACKAGE_TARNAME \"$PACKAGE_TARNAME\"" >>confdefs.h printf "%s\n" "#define PACKAGE_VERSION \"$PACKAGE_VERSION\"" >>confdefs.h printf "%s\n" "#define PACKAGE_STRING \"$PACKAGE_STRING\"" >>confdefs.h printf "%s\n" "#define PACKAGE_BUGREPORT \"$PACKAGE_BUGREPORT\"" >>confdefs.h printf "%s\n" "#define PACKAGE_URL \"$PACKAGE_URL\"" >>confdefs.h # Let the site file select an alternate cache file if it wants to. # Prefer an explicitly selected file to automatically selected ones. if test -n "$CONFIG_SITE"; then ac_site_files="$CONFIG_SITE" elif test "x$prefix" != xNONE; then ac_site_files="$prefix/share/config.site $prefix/etc/config.site" else ac_site_files="$ac_default_prefix/share/config.site $ac_default_prefix/etc/config.site" fi for ac_site_file in $ac_site_files do case $ac_site_file in #( */*) : ;; #( *) : ac_site_file=./$ac_site_file ;; esac if test -f "$ac_site_file" && test -r "$ac_site_file"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 printf "%s\n" "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ || { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "failed to load site script $ac_site_file See \`config.log' for more details" "$LINENO" 5; } fi done if test -r "$cache_file"; then # Some versions of bash will fail to source /dev/null (special files # actually), so we avoid doing that. DJGPP emulates it as a regular file. if test /dev/null != "$cache_file" && test -f "$cache_file"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 printf "%s\n" "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 printf "%s\n" "$as_me: creating cache $cache_file" >&6;} >$cache_file fi # Auxiliary files required by this configure script. ac_aux_files="install-sh" # Locations in which to look for auxiliary files. ac_aux_dir_candidates="${srcdir}${PATH_SEPARATOR}${srcdir}/..${PATH_SEPARATOR}${srcdir}/../.." # Search for a directory containing all of the required auxiliary files, # $ac_aux_files, from the $PATH-style list $ac_aux_dir_candidates. # If we don't find one directory that contains all the files we need, # we report the set of missing files from the *first* directory in # $ac_aux_dir_candidates and give up. ac_missing_aux_files="" ac_first_candidate=: printf "%s\n" "$as_me:${as_lineno-$LINENO}: looking for aux files: $ac_aux_files" >&5 as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_found=false for as_dir in $ac_aux_dir_candidates do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac as_found=: printf "%s\n" "$as_me:${as_lineno-$LINENO}: trying $as_dir" >&5 ac_aux_dir_found=yes ac_install_sh= for ac_aux in $ac_aux_files do # As a special case, if "install-sh" is required, that requirement # can be satisfied by any of "install-sh", "install.sh", or "shtool", # and $ac_install_sh is set appropriately for whichever one is found. if test x"$ac_aux" = x"install-sh" then if test -f "${as_dir}install-sh"; then printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}install-sh found" >&5 ac_install_sh="${as_dir}install-sh -c" elif test -f "${as_dir}install.sh"; then printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}install.sh found" >&5 ac_install_sh="${as_dir}install.sh -c" elif test -f "${as_dir}shtool"; then printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}shtool found" >&5 ac_install_sh="${as_dir}shtool install -c" else ac_aux_dir_found=no if $ac_first_candidate; then ac_missing_aux_files="${ac_missing_aux_files} install-sh" else break fi fi else if test -f "${as_dir}${ac_aux}"; then printf "%s\n" "$as_me:${as_lineno-$LINENO}: ${as_dir}${ac_aux} found" >&5 else ac_aux_dir_found=no if $ac_first_candidate; then ac_missing_aux_files="${ac_missing_aux_files} ${ac_aux}" else break fi fi fi done if test "$ac_aux_dir_found" = yes; then ac_aux_dir="$as_dir" break fi ac_first_candidate=false as_found=false done IFS=$as_save_IFS if $as_found then : else $as_nop as_fn_error $? "cannot find required auxiliary files:$ac_missing_aux_files" "$LINENO" 5 fi # These three variables are undocumented and unsupported, # and are intended to be withdrawn in a future Autoconf release. # They can cause serious problems if a builder's source tree is in a directory # whose full name contains unusual characters. if test -f "${ac_aux_dir}config.guess"; then ac_config_guess="$SHELL ${ac_aux_dir}config.guess" fi if test -f "${ac_aux_dir}config.sub"; then ac_config_sub="$SHELL ${ac_aux_dir}config.sub" fi if test -f "$ac_aux_dir/configure"; then ac_configure="$SHELL ${ac_aux_dir}configure" fi # Check that the precious variables saved in the cache have kept the same # value. ac_cache_corrupted=false for ac_var in $ac_precious_vars; do eval ac_old_set=\$ac_cv_env_${ac_var}_set eval ac_new_set=\$ac_env_${ac_var}_set eval ac_old_val=\$ac_cv_env_${ac_var}_value eval ac_new_val=\$ac_env_${ac_var}_value case $ac_old_set,$ac_new_set in set,) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 printf "%s\n" "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} ac_cache_corrupted=: ;; ,set) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 printf "%s\n" "$as_me: error: \`$ac_var' was not set in the previous run" >&2;} ac_cache_corrupted=: ;; ,);; *) if test "x$ac_old_val" != "x$ac_new_val"; then # differences in whitespace do not lead to failure. ac_old_val_w=`echo x $ac_old_val` ac_new_val_w=`echo x $ac_new_val` if test "$ac_old_val_w" != "$ac_new_val_w"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 printf "%s\n" "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} ac_cache_corrupted=: else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 printf "%s\n" "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} eval $ac_var=\$ac_old_val fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 printf "%s\n" "$as_me: former value: \`$ac_old_val'" >&2;} { printf "%s\n" "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 printf "%s\n" "$as_me: current value: \`$ac_new_val'" >&2;} fi;; esac # Pass precious variables to config.status. if test "$ac_new_set" = set; then case $ac_new_val in *\'*) ac_arg=$ac_var=`printf "%s\n" "$ac_new_val" | sed "s/'/'\\\\\\\\''/g"` ;; *) ac_arg=$ac_var=$ac_new_val ;; esac case " $ac_configure_args " in *" '$ac_arg' "*) ;; # Avoid dups. Use of quotes ensures accuracy. *) as_fn_append ac_configure_args " '$ac_arg'" ;; esac fi done if $ac_cache_corrupted; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 printf "%s\n" "$as_me: error: changes in the environment can compromise the build" >&2;} as_fn_error $? "run \`${MAKE-make} distclean' and/or \`rm $cache_file' and start over" "$LINENO" 5 fi ## -------------------- ## ## Main body of script. ## ## -------------------- ## ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' ac_compiler_gnu=$ac_cv_c_compiler_gnu if test -r "$cache_file"; then # Some versions of bash will fail to source /dev/null (special files # actually), so we avoid doing that. DJGPP emulates it as a regular file. if test /dev/null != "$cache_file" && test -f "$cache_file"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 printf "%s\n" "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 printf "%s\n" "$as_me: creating cache $cache_file" >&6;} >$cache_file fi CONFIG_STATUS_DEPENDENCIES=vsn.mk CONFIGURE_DEPENDENCIES=vsn.mk # Extract the first word of "sed", so it can be a program name with args. set dummy sed; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_SED+y} then : printf %s "(cached) " >&6 else $as_nop case $SED in [\\/]* | ?:[\\/]*) ac_cv_path_SED="$SED" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_SED="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi SED=$ac_cv_path_SED if test -n "$SED"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $SED" >&5 printf "%s\n" "$SED" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi ac_ext=erl : ${ac_objext=o} ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && ln -sf conftest.beam conftest.$ac_objext' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && printf "%s\n" "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' # Check whether --with-erlang was given. if test ${with_erlang+y} then : withval=$with_erlang; fi if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}erlc", so it can be a program name with args. set dummy ${ac_tool_prefix}erlc; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ERLC+y} then : printf %s "(cached) " >&6 else $as_nop case $ERLC in [\\/]* | ?:[\\/]*) ac_cv_path_ERLC="$ERLC" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_dummy="$with_erlang:$with_erlang/bin:$PATH" for as_dir in $as_dummy do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ERLC="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ERLC=$ac_cv_path_ERLC if test -n "$ERLC"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ERLC" >&5 printf "%s\n" "$ERLC" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi fi if test -z "$ac_cv_path_ERLC"; then ac_pt_ERLC=$ERLC # Extract the first word of "erlc", so it can be a program name with args. set dummy erlc; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ac_pt_ERLC+y} then : printf %s "(cached) " >&6 else $as_nop case $ac_pt_ERLC in [\\/]* | ?:[\\/]*) ac_cv_path_ac_pt_ERLC="$ac_pt_ERLC" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_dummy="$with_erlang:$with_erlang/bin:$PATH" for as_dir in $as_dummy do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ac_pt_ERLC="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ac_pt_ERLC=$ac_cv_path_ac_pt_ERLC if test -n "$ac_pt_ERLC"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERLC" >&5 printf "%s\n" "$ac_pt_ERLC" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi if test "x$ac_pt_ERLC" = x; then ERLC="erlc" else case $cross_compiling:$ac_tool_warned in yes:) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac ERLC=$ac_pt_ERLC fi else ERLC="$ac_cv_path_ERLC" fi if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}erl", so it can be a program name with args. set dummy ${ac_tool_prefix}erl; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ERL+y} then : printf %s "(cached) " >&6 else $as_nop case $ERL in [\\/]* | ?:[\\/]*) ac_cv_path_ERL="$ERL" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_dummy="$with_erlang:$with_erlang/bin:$PATH" for as_dir in $as_dummy do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ERL="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ERL=$ac_cv_path_ERL if test -n "$ERL"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ERL" >&5 printf "%s\n" "$ERL" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi fi if test -z "$ac_cv_path_ERL"; then ac_pt_ERL=$ERL # Extract the first word of "erl", so it can be a program name with args. set dummy erl; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ac_pt_ERL+y} then : printf %s "(cached) " >&6 else $as_nop case $ac_pt_ERL in [\\/]* | ?:[\\/]*) ac_cv_path_ac_pt_ERL="$ac_pt_ERL" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_dummy="$with_erlang:$with_erlang/bin:$PATH" for as_dir in $as_dummy do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ac_pt_ERL="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ac_pt_ERL=$ac_cv_path_ac_pt_ERL if test -n "$ac_pt_ERL"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERL" >&5 printf "%s\n" "$ac_pt_ERL" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi if test "x$ac_pt_ERL" = x; then ERL="erl" else case $cross_compiling:$ac_tool_warned in yes:) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac ERL=$ac_pt_ERL fi else ERL="$ac_cv_path_ERL" fi # Extract the first word of "dialyzer", so it can be a program name with args. set dummy dialyzer; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_DIALYZER+y} then : printf %s "(cached) " >&6 else $as_nop case $DIALYZER in [\\/]* | ?:[\\/]*) ac_cv_path_DIALYZER="$DIALYZER" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR as_dummy="$with_erlang:$with_erlang/bin:$PATH" for as_dir in $as_dummy do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_DIALYZER="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS test -z "$ac_cv_path_DIALYZER" && ac_cv_path_DIALYZER="/usr/bin/dializer" ;; esac fi DIALYZER=$ac_cv_path_DIALYZER if test -n "$DIALYZER"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $DIALYZER" >&5 printf "%s\n" "$DIALYZER" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi if test "x$prefix" = xNONE; then printf %s "checking for prefix by " >&6 # Extract the first word of "erl", so it can be a program name with args. set dummy erl; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ac_prefix_program+y} then : printf %s "(cached) " >&6 else $as_nop case $ac_prefix_program in [\\/]* | ?:[\\/]*) ac_cv_path_ac_prefix_program="$ac_prefix_program" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ac_prefix_program="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ac_prefix_program=$ac_cv_path_ac_prefix_program if test -n "$ac_prefix_program"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_prefix_program" >&5 printf "%s\n" "$ac_prefix_program" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi if test -n "$ac_prefix_program"; then prefix=`$as_dirname -- "$ac_prefix_program" || $as_expr X"$ac_prefix_program" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$ac_prefix_program" : 'X\(//\)[^/]' \| \ X"$ac_prefix_program" : 'X\(//\)$' \| \ X"$ac_prefix_program" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X"$ac_prefix_program" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` prefix=`$as_dirname -- "$prefix" || $as_expr X"$prefix" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$prefix" : 'X\(//\)[^/]' \| \ X"$prefix" : 'X\(//\)$' \| \ X"$prefix" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X"$prefix" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` fi fi if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}erl", so it can be a program name with args. set dummy ${ac_tool_prefix}erl; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ERL+y} then : printf %s "(cached) " >&6 else $as_nop case $ERL in [\\/]* | ?:[\\/]*) ac_cv_path_ERL="$ERL" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ERL="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ERL=$ac_cv_path_ERL if test -n "$ERL"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ERL" >&5 printf "%s\n" "$ERL" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi fi if test -z "$ac_cv_path_ERL"; then ac_pt_ERL=$ERL # Extract the first word of "erl", so it can be a program name with args. set dummy erl; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ac_pt_ERL+y} then : printf %s "(cached) " >&6 else $as_nop case $ac_pt_ERL in [\\/]* | ?:[\\/]*) ac_cv_path_ac_pt_ERL="$ac_pt_ERL" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ac_pt_ERL="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ac_pt_ERL=$ac_cv_path_ac_pt_ERL if test -n "$ac_pt_ERL"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERL" >&5 printf "%s\n" "$ac_pt_ERL" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi if test "x$ac_pt_ERL" = x; then ERL="not found" else case $cross_compiling:$ac_tool_warned in yes:) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac ERL=$ac_pt_ERL fi else ERL="$ac_cv_path_ERL" fi if test "$ERL" = "not found"; then as_fn_error 77 "Erlang/OTP interpreter (erl) not found but required" "$LINENO" 5 fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP root directory" >&5 printf %s "checking for Erlang/OTP root directory... " >&6; } if test ${ac_cv_erlang_root_dir+y} then : printf %s "(cached) " >&6 else $as_nop ac_cv_erlang_root_dir=`$ERL -noshell -eval ' io:format("~s~n", [code:root_dir()]), halt(0) '` fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_root_dir" >&5 printf "%s\n" "$ac_cv_erlang_root_dir" >&6; } ERLANG_ROOT_DIR=$ac_cv_erlang_root_dir { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'xmerl' library subdirectory" >&5 printf %s "checking for Erlang/OTP 'xmerl' library subdirectory... " >&6; } if test ${ac_cv_erlang_lib_dir_xmerl+y} then : printf %s "(cached) " >&6 else $as_nop ac_cv_erlang_lib_dir_xmerl=`$ERL -noshell -eval ' case code:lib_dir("xmerl") of {error, bad_name} -> io:format("not found~n"); LibDir -> io:format("~s~n", [LibDir]) end, halt(0) '` fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_xmerl" >&5 printf "%s\n" "$ac_cv_erlang_lib_dir_xmerl" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'xmerl' library version" >&5 printf %s "checking for Erlang/OTP 'xmerl' library version... " >&6; } if test ${ac_cv_erlang_lib_ver_xmerl+y} then : printf %s "(cached) " >&6 else $as_nop if test "$ac_cv_erlang_lib_dir_xmerl" = "not found" then : ac_cv_erlang_lib_ver_xmerl="not found" else $as_nop ac_cv_erlang_lib_ver_xmerl=`printf "%s\n" "$ac_cv_erlang_lib_dir_xmerl" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_xmerl" >&5 printf "%s\n" "$ac_cv_erlang_lib_ver_xmerl" >&6; } ERLANG_LIB_DIR_xmerl=$ac_cv_erlang_lib_dir_xmerl ERLANG_LIB_VER_xmerl=$ac_cv_erlang_lib_ver_xmerl if test "$ac_cv_erlang_lib_dir_xmerl" = "not found" then : fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'ssl' library subdirectory" >&5 printf %s "checking for Erlang/OTP 'ssl' library subdirectory... " >&6; } if test ${ac_cv_erlang_lib_dir_ssl+y} then : printf %s "(cached) " >&6 else $as_nop ac_cv_erlang_lib_dir_ssl=`$ERL -noshell -eval ' case code:lib_dir("ssl") of {error, bad_name} -> io:format("not found~n"); LibDir -> io:format("~s~n", [LibDir]) end, halt(0) '` fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_ssl" >&5 printf "%s\n" "$ac_cv_erlang_lib_dir_ssl" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'ssl' library version" >&5 printf %s "checking for Erlang/OTP 'ssl' library version... " >&6; } if test ${ac_cv_erlang_lib_ver_ssl+y} then : printf %s "(cached) " >&6 else $as_nop if test "$ac_cv_erlang_lib_dir_ssl" = "not found" then : ac_cv_erlang_lib_ver_ssl="not found" else $as_nop ac_cv_erlang_lib_ver_ssl=`printf "%s\n" "$ac_cv_erlang_lib_dir_ssl" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_ssl" >&5 printf "%s\n" "$ac_cv_erlang_lib_ver_ssl" >&6; } ERLANG_LIB_DIR_ssl=$ac_cv_erlang_lib_dir_ssl ERLANG_LIB_VER_ssl=$ac_cv_erlang_lib_ver_ssl if test "$ac_cv_erlang_lib_dir_ssl" = "not found" then : fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'crypto' library subdirectory" >&5 printf %s "checking for Erlang/OTP 'crypto' library subdirectory... " >&6; } if test ${ac_cv_erlang_lib_dir_crypto+y} then : printf %s "(cached) " >&6 else $as_nop ac_cv_erlang_lib_dir_crypto=`$ERL -noshell -eval ' case code:lib_dir("crypto") of {error, bad_name} -> io:format("not found~n"); LibDir -> io:format("~s~n", [LibDir]) end, halt(0) '` fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_crypto" >&5 printf "%s\n" "$ac_cv_erlang_lib_dir_crypto" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'crypto' library version" >&5 printf %s "checking for Erlang/OTP 'crypto' library version... " >&6; } if test ${ac_cv_erlang_lib_ver_crypto+y} then : printf %s "(cached) " >&6 else $as_nop if test "$ac_cv_erlang_lib_dir_crypto" = "not found" then : ac_cv_erlang_lib_ver_crypto="not found" else $as_nop ac_cv_erlang_lib_ver_crypto=`printf "%s\n" "$ac_cv_erlang_lib_dir_crypto" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_crypto" >&5 printf "%s\n" "$ac_cv_erlang_lib_ver_crypto" >&6; } ERLANG_LIB_DIR_crypto=$ac_cv_erlang_lib_dir_crypto ERLANG_LIB_VER_crypto=$ac_cv_erlang_lib_ver_crypto if test "$ac_cv_erlang_lib_dir_crypto" = "not found" then : fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'public_key' library subdirectory" >&5 printf %s "checking for Erlang/OTP 'public_key' library subdirectory... " >&6; } if test ${ac_cv_erlang_lib_dir_public_key+y} then : printf %s "(cached) " >&6 else $as_nop ac_cv_erlang_lib_dir_public_key=`$ERL -noshell -eval ' case code:lib_dir("public_key") of {error, bad_name} -> io:format("not found~n"); LibDir -> io:format("~s~n", [LibDir]) end, halt(0) '` fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_public_key" >&5 printf "%s\n" "$ac_cv_erlang_lib_dir_public_key" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'public_key' library version" >&5 printf %s "checking for Erlang/OTP 'public_key' library version... " >&6; } if test ${ac_cv_erlang_lib_ver_public_key+y} then : printf %s "(cached) " >&6 else $as_nop if test "$ac_cv_erlang_lib_dir_public_key" = "not found" then : ac_cv_erlang_lib_ver_public_key="not found" else $as_nop ac_cv_erlang_lib_ver_public_key=`printf "%s\n" "$ac_cv_erlang_lib_dir_public_key" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_public_key" >&5 printf "%s\n" "$ac_cv_erlang_lib_ver_public_key" >&6; } ERLANG_LIB_DIR_public_key=$ac_cv_erlang_lib_dir_public_key ERLANG_LIB_VER_public_key=$ac_cv_erlang_lib_ver_public_key if test "$ac_cv_erlang_lib_dir_public_key" = "not found" then : fi if test -n "$ac_tool_prefix"; then # Extract the first word of "${ac_tool_prefix}erlc", so it can be a program name with args. set dummy ${ac_tool_prefix}erlc; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ERLC+y} then : printf %s "(cached) " >&6 else $as_nop case $ERLC in [\\/]* | ?:[\\/]*) ac_cv_path_ERLC="$ERLC" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ERLC="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ERLC=$ac_cv_path_ERLC if test -n "$ERLC"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ERLC" >&5 printf "%s\n" "$ERLC" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi fi if test -z "$ac_cv_path_ERLC"; then ac_pt_ERLC=$ERLC # Extract the first word of "erlc", so it can be a program name with args. set dummy erlc; ac_word=$2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 printf %s "checking for $ac_word... " >&6; } if test ${ac_cv_path_ac_pt_ERLC+y} then : printf %s "(cached) " >&6 else $as_nop case $ac_pt_ERLC in [\\/]* | ?:[\\/]*) ac_cv_path_ac_pt_ERLC="$ac_pt_ERLC" # Let the user override the test with a path. ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_word$ac_exec_ext"; then ac_cv_path_ac_pt_ERLC="$as_dir$ac_word$ac_exec_ext" printf "%s\n" "$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext" >&5 break 2 fi done done IFS=$as_save_IFS ;; esac fi ac_pt_ERLC=$ac_cv_path_ac_pt_ERLC if test -n "$ac_pt_ERLC"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERLC" >&5 printf "%s\n" "$ac_pt_ERLC" >&6; } else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } fi if test "x$ac_pt_ERLC" = x; then ERLC="not found" else case $cross_compiling:$ac_tool_warned in yes:) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 printf "%s\n" "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} ac_tool_warned=yes ;; esac ERLC=$ac_pt_ERLC fi else ERLC="$ac_cv_path_ERLC" fi if test "$ERLC" = "not found"; then as_fn_error 77 "Erlang/OTP compiler (erlc) not found but required" "$LINENO" 5 fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if Erlang/OTP SSL application is running fine" >&5 printf %s "checking if Erlang/OTP SSL application is running fine... " >&6; } if test ${erlang_cv_ssl_runnable+y} then : printf %s "(cached) " >&6 else $as_nop erlang_cv_ssl_runnable=no if test "$cross_compiling" = yes then : { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling See \`config.log' for more details" "$LINENO" 5; } else $as_nop cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> case application:start(ssl) of ok -> ok; Err -> halt(1) end, halt(0) . _ACEOF if ac_fn_erl_try_run "$LINENO" then : erlang_cv_ssl_runnable=yes ERLANG_APPLICATIONS="kernel,stdlib,ssl" else $as_nop if test "$cross_compiling" = yes then : { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling See \`config.log' for more details" "$LINENO" 5; } else $as_nop cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> application:start(crypto), application:start(asn1), application:start(public_key), case application:start(ssl) of ok -> ok; Err -> halt(1) end, halt(0) . _ACEOF if ac_fn_erl_try_run "$LINENO" then : erlang_cv_ssl_runnable=yes ERLANG_APPLICATIONS="kernel,stdlib,asn1,crypto,public_key,ssl" else $as_nop ERLANG_APPLICATIONS="kernel,stdlib" { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: WARNING: ssl application is not working properly !!!" >&5 printf "%s\n" "WARNING: ssl application is not working properly !!!" >&6; } fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $erlang_cv_ssl_runnable" >&5 printf "%s\n" "$erlang_cv_ssl_runnable" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if Erlang/OTP crypto application is running fine" >&5 printf %s "checking if Erlang/OTP crypto application is running fine... " >&6; } if test ${erlang_cv_crypto_runnable+y} then : printf %s "(cached) " >&6 else $as_nop erlang_cv_crypto_runnable=no if test "$cross_compiling" = yes then : { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling See \`config.log' for more details" "$LINENO" 5; } else $as_nop cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> case application:start(crypto) of ok -> case catch crypto:hash(md5, "toto") of <<247,29,190,82,98,138,63,131,167,122,180,148,129,117,37, 198>> -> ok; _ -> halt(1) end; Err -> erlang:display(Err), halt(1) end, halt(0) . _ACEOF if ac_fn_erl_try_run "$LINENO" then : erlang_cv_crypto_runnable=yes ERLANG_APPLICATIONS="$ERLANG_APPLICATIONS,crypto" else $as_nop { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: WARNING: crypto application is not working properly !!!" >&5 printf "%s\n" "WARNING: crypto application is not working properly !!!" >&6; } fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $erlang_cv_crypto_runnable" >&5 printf "%s\n" "$erlang_cv_crypto_runnable" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking new time API" >&5 printf %s "checking new time API... " >&6; } if test ${erlang_cv_new_time_api+y} then : printf %s "(cached) " >&6 else $as_nop erlang_cv_new_time_api=no if test "$cross_compiling" = yes then : { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 printf "%s\n" "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "cannot run test program while cross compiling See \`config.log' for more details" "$LINENO" 5; } else $as_nop cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> R=case catch erlang:timestamp() of {A,B,C} -> 0; _ -> 1 end, halt(R) . _ACEOF if ac_fn_erl_try_run "$LINENO" then : erlang_cv_new_time_api=yes else $as_nop { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: WARNING: new time API not available. use old now() instead" >&5 printf "%s\n" "WARNING: new time API not available. use old now() instead" >&6; } fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $erlang_cv_new_time_api" >&5 printf "%s\n" "$erlang_cv_new_time_api" >&6; } DTD=tsung-1.0.dtd TEMPLATES_SUBDIR=tsung/templates { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5 printf %s "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; } set x ${MAKE-make} ac_make=`printf "%s\n" "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'` if eval test \${ac_cv_prog_make_${ac_make}_set+y} then : printf %s "(cached) " >&6 else $as_nop cat >conftest.make <<\_ACEOF SHELL = /bin/sh all: @echo '@@@%%%=$(MAKE)=@@@%%%' _ACEOF # GNU make sometimes prints "make[1]: Entering ...", which would confuse us. case `${MAKE-make} -f conftest.make 2>/dev/null` in *@@@%%%=?*=@@@%%%*) eval ac_cv_prog_make_${ac_make}_set=yes;; *) eval ac_cv_prog_make_${ac_make}_set=no;; esac rm -f conftest.make fi if eval test \$ac_cv_prog_make_${ac_make}_set = yes; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 printf "%s\n" "yes" >&6; } SET_MAKE= else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } SET_MAKE="MAKE=${MAKE-make}" fi # Find a good install program. We prefer a C program (faster), # so one script is as good as another. But avoid the broken or # incompatible versions: # SysV /etc/install, /usr/sbin/install # SunOS /usr/etc/install # IRIX /sbin/install # AIX /bin/install # AmigaOS /C/install, which installs bootblocks on floppy discs # AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag # AFS /usr/afsws/bin/install, which mishandles nonexistent args # SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff" # OS/2's system install, which has a completely different semantic # ./install, which can be erroneously created by make from ./install.sh. # Reject install programs that cannot install multiple files. { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 printf %s "checking for a BSD-compatible install... " >&6; } if test -z "$INSTALL"; then if test ${ac_cv_path_install+y} then : printf %s "(cached) " >&6 else $as_nop as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac # Account for fact that we put trailing slashes in our PATH walk. case $as_dir in #(( ./ | /[cC]/* | \ /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ /usr/ucb/* ) ;; *) # OSF1 and SCO ODT 3.0 have their own names for install. # Don't use installbsd from OSF since it installs stuff as root # by default. for ac_prog in ginstall scoinst install; do for ac_exec_ext in '' $ac_executable_extensions; do if as_fn_executable_p "$as_dir$ac_prog$ac_exec_ext"; then if test $ac_prog = install && grep dspmsg "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then # AIX install. It has an incompatible calling convention. : elif test $ac_prog = install && grep pwplus "$as_dir$ac_prog$ac_exec_ext" >/dev/null 2>&1; then # program-specific install script used by HP pwplus--don't use. : else rm -rf conftest.one conftest.two conftest.dir echo one > conftest.one echo two > conftest.two mkdir conftest.dir if "$as_dir$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir/" && test -s conftest.one && test -s conftest.two && test -s conftest.dir/conftest.one && test -s conftest.dir/conftest.two then ac_cv_path_install="$as_dir$ac_prog$ac_exec_ext -c" break 3 fi fi fi done done ;; esac done IFS=$as_save_IFS rm -rf conftest.one conftest.two conftest.dir fi if test ${ac_cv_path_install+y}; then INSTALL=$ac_cv_path_install else # As a last resort, use the slow shell script. Don't cache a # value for INSTALL within a source directory, because that will # break other packages using the cache if that directory is # removed, or if the value is a relative name. INSTALL=$ac_install_sh fi fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 printf "%s\n" "$INSTALL" >&6; } # Use test -z because SunOS4 sh mishandles braces in ${var-val}. # It thinks the first close brace ends the variable substitution. test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}' test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' EXP_VAR=EXPANDED_LIBDIR FROM_VAR="$libdir" prefix_save=$prefix exec_prefix_save=$exec_prefix if test "x$prefix" = "xNONE"; then prefix="$ac_default_prefix" fi if test "x$exec_prefix" = "xNONE"; then exec_prefix=$prefix fi full_var="$FROM_VAR" while true; do new_full_var="`eval echo $full_var`" if test "x$new_full_var" = "x$full_var"; then break; fi full_var=$new_full_var done full_var=$new_full_var EXPANDED_LIBDIR="$full_var" prefix=$prefix_save exec_prefix=$exec_prefix_save { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Storing library files in $EXPANDED_LIBDIR" >&5 printf "%s\n" "$as_me: Storing library files in $EXPANDED_LIBDIR" >&6;} EXP_VAR=EXPANDED_SHAREDIR FROM_VAR="$datadir/tsung" prefix_save=$prefix exec_prefix_save=$exec_prefix if test "x$prefix" = "xNONE"; then prefix="$ac_default_prefix" fi if test "x$exec_prefix" = "xNONE"; then exec_prefix=$prefix fi full_var="$FROM_VAR" while true; do new_full_var="`eval echo $full_var`" if test "x$new_full_var" = "x$full_var"; then break; fi full_var=$new_full_var done full_var=$new_full_var EXPANDED_SHAREDIR="$full_var" prefix=$prefix_save exec_prefix=$exec_prefix_save { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Storing data files in $EXPANDED_SHAREDIR" >&5 printf "%s\n" "$as_me: Storing data files in $EXPANDED_SHAREDIR" >&6;} ac_config_files="$ac_config_files Makefile tsung.spec tsung.sh tsung-recorder.sh examples/*.xml src/tsung_stats.pl src/tsung-plotter/tsplot.py src/log2tsung.pl src/tsung_controller/tsung_controller.app src/tsung_recorder/tsung_recorder.app src/tsung/tsung.app" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure # tests run on this system so they can be shared between configure # scripts and configure runs, see configure's option --config-cache. # It is not useful on other systems. If it contains results you don't # want to keep, you may remove or edit it. # # config.status only pays attention to the cache file if you give it # the --recheck option to rerun configure. # # `ac_cv_env_foo' variables (set or unset) will be overridden when # loading this file, other *unset* `ac_cv_foo' will be assigned the # following values. _ACEOF # The following way of writing the cache mishandles newlines in values, # but we know of no workaround that is simple, portable, and efficient. # So, we kill variables containing newlines. # Ultrix sh set writes to stderr and can't be redirected directly, # and sets the high bit in the cache file unless we assign to the vars. ( for ac_var in `(set) 2>&1 | sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\)=.*/\1/p'`; do eval ac_val=\$$ac_var case $ac_val in #( *${as_nl}*) case $ac_var in #( *_cv_*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 printf "%s\n" "$as_me: WARNING: cache variable $ac_var contains a newline" >&2;} ;; esac case $ac_var in #( _ | IFS | as_nl) ;; #( BASH_ARGV | BASH_SOURCE) eval $ac_var= ;; #( *) { eval $ac_var=; unset $ac_var;} ;; esac ;; esac done (set) 2>&1 | case $as_nl`(ac_space=' '; set) 2>&1` in #( *${as_nl}ac_space=\ *) # `set' does not quote correctly, so add quotes: double-quote # substitution turns \\\\ into \\, and sed turns \\ into \. sed -n \ "s/'/'\\\\''/g; s/^\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\)=\\(.*\\)/\\1='\\2'/p" ;; #( *) # `set' quotes correctly as required by POSIX, so do not add quotes. sed -n "/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p" ;; esac | sort ) | sed ' /^ac_cv_env_/b end t clear :clear s/^\([^=]*\)=\(.*[{}].*\)$/test ${\1+y} || &/ t end s/^\([^=]*\)=\(.*\)$/\1=${\1=\2}/ :end' >>confcache if diff "$cache_file" confcache >/dev/null 2>&1; then :; else if test -w "$cache_file"; then if test "x$cache_file" != "x/dev/null"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 printf "%s\n" "$as_me: updating cache $cache_file" >&6;} if test ! -f "$cache_file" || test -h "$cache_file"; then cat confcache >"$cache_file" else case $cache_file in #( */* | ?:*) mv -f confcache "$cache_file"$$ && mv -f "$cache_file"$$ "$cache_file" ;; #( *) mv -f confcache "$cache_file" ;; esac fi fi else { printf "%s\n" "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 printf "%s\n" "$as_me: not updating unwritable cache $cache_file" >&6;} fi fi rm -f confcache test "x$prefix" = xNONE && prefix=$ac_default_prefix # Let make expand exec_prefix. test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' # Transform confdefs.h into DEFS. # Protect against shell expansion while executing Makefile rules. # Protect against Makefile macro expansion. # # If the first sed substitution is executed (which looks for macros that # take arguments), then branch to the quote section. Otherwise, # look for a macro that doesn't take arguments. ac_script=' :mline /\\$/{ N s,\\\n,, b mline } t clear :clear s/^[ ]*#[ ]*define[ ][ ]*\([^ (][^ (]*([^)]*)\)[ ]*\(.*\)/-D\1=\2/g t quote s/^[ ]*#[ ]*define[ ][ ]*\([^ ][^ ]*\)[ ]*\(.*\)/-D\1=\2/g t quote b any :quote s/[ `~#$^&*(){}\\|;'\''"<>?]/\\&/g s/\[/\\&/g s/\]/\\&/g s/\$/$$/g H :any ${ g s/^\n// s/\n/ /g p } ' DEFS=`sed -n "$ac_script" confdefs.h` ac_libobjs= ac_ltlibobjs= U= for ac_i in : $LIBOBJS; do test "x$ac_i" = x: && continue # 1. Remove the extension, and $U if already installed. ac_script='s/\$U\././;s/\.o$//;s/\.obj$//' ac_i=`printf "%s\n" "$ac_i" | sed "$ac_script"` # 2. Prepend LIBOBJDIR. When used with automake>=1.10 LIBOBJDIR # will be set to the directory where LIBOBJS objects are built. as_fn_append ac_libobjs " \${LIBOBJDIR}$ac_i\$U.$ac_objext" as_fn_append ac_ltlibobjs " \${LIBOBJDIR}$ac_i"'$U.lo' done LIBOBJS=$ac_libobjs LTLIBOBJS=$ac_ltlibobjs : "${CONFIG_STATUS=./config.status}" ac_write_fail=0 ac_clean_files_save=$ac_clean_files ac_clean_files="$ac_clean_files $CONFIG_STATUS" { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 printf "%s\n" "$as_me: creating $CONFIG_STATUS" >&6;} as_write_fail=0 cat >$CONFIG_STATUS <<_ASEOF || as_write_fail=1 #! $SHELL # Generated by $as_me. # Run this file to recreate the current configuration. # Compiler output produced by configure, useful for debugging # configure, is in config.log if it exists. debug=false ac_cs_recheck=false ac_cs_silent=false SHELL=\${CONFIG_SHELL-$SHELL} export SHELL _ASEOF cat >>$CONFIG_STATUS <<\_ASEOF || as_write_fail=1 ## -------------------- ## ## M4sh Initialization. ## ## -------------------- ## # Be more Bourne compatible DUALCASE=1; export DUALCASE # for MKS sh as_nop=: if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1 then : emulate sh NULLCMD=: # Pre-4.2 versions of Zsh do word splitting on ${1+"$@"}, which # is contrary to our usage. Disable this feature. alias -g '${1+"$@"}'='"$@"' setopt NO_GLOB_SUBST else $as_nop case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi # Reset variables that may have inherited troublesome values from # the environment. # IFS needs to be set, to space, tab, and newline, in precisely that order. # (If _AS_PATH_WALK were called with IFS unset, it would have the # side effect of setting IFS to empty, thus disabling word splitting.) # Quoting is to prevent editors from complaining about space-tab. as_nl=' ' export as_nl IFS=" "" $as_nl" PS1='$ ' PS2='> ' PS4='+ ' # Ensure predictable behavior from utilities with locale-dependent output. LC_ALL=C export LC_ALL LANGUAGE=C export LANGUAGE # We cannot yet rely on "unset" to work, but we need these variables # to be unset--not just set to an empty or harmless value--now, to # avoid bugs in old shells (e.g. pre-3.0 UWIN ksh). This construct # also avoids known problems related to "unset" and subshell syntax # in other old shells (e.g. bash 2.01 and pdksh 5.2.14). for as_var in BASH_ENV ENV MAIL MAILPATH CDPATH do eval test \${$as_var+y} \ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : done # Ensure that fds 0, 1, and 2 are open. if (exec 3>&0) 2>/dev/null; then :; else exec 0&1) 2>/dev/null; then :; else exec 1>/dev/null; fi if (exec 3>&2) ; then :; else exec 2>/dev/null; fi # The user is always right. if ${PATH_SEPARATOR+false} :; then PATH_SEPARATOR=: (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 || PATH_SEPARATOR=';' } fi # Find who we are. Look in the path if we contain no directory separator. as_myself= case $0 in #(( *[\\/]* ) as_myself=$0 ;; *) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS case $as_dir in #((( '') as_dir=./ ;; */) ;; *) as_dir=$as_dir/ ;; esac test -r "$as_dir$0" && as_myself=$as_dir$0 && break done IFS=$as_save_IFS ;; esac # We did not find ourselves, most probably we were run as `sh COMMAND' # in which case we are not to be found in the path. if test "x$as_myself" = x; then as_myself=$0 fi if test ! -f "$as_myself"; then printf "%s\n" "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi # as_fn_error STATUS ERROR [LINENO LOG_FD] # ---------------------------------------- # Output "`basename $0`: error: ERROR" to stderr. If LINENO and LOG_FD are # provided, also output the error to LOG_FD, referencing LINENO. Then exit the # script with STATUS, using 1 if that was 0. as_fn_error () { as_status=$1; test $as_status -eq 0 && as_status=1 if test "$4"; then as_lineno=${as_lineno-"$3"} as_lineno_stack=as_lineno_stack=$as_lineno_stack printf "%s\n" "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi printf "%s\n" "$as_me: error: $2" >&2 as_fn_exit $as_status } # as_fn_error # as_fn_set_status STATUS # ----------------------- # Set $? to STATUS, without forking. as_fn_set_status () { return $1 } # as_fn_set_status # as_fn_exit STATUS # ----------------- # Exit the shell with STATUS, even in a "trap 0" or "set -e" context. as_fn_exit () { set +e as_fn_set_status $1 exit $1 } # as_fn_exit # as_fn_unset VAR # --------------- # Portably unset VAR. as_fn_unset () { { eval $1=; unset $1;} } as_unset=as_fn_unset # as_fn_append VAR VALUE # ---------------------- # Append the text in VALUE to the end of the definition contained in VAR. Take # advantage of any shell optimizations that allow amortized linear growth over # repeated appends, instead of the typical quadratic growth present in naive # implementations. if (eval "as_var=1; as_var+=2; test x\$as_var = x12") 2>/dev/null then : eval 'as_fn_append () { eval $1+=\$2 }' else $as_nop as_fn_append () { eval $1=\$$1\$2 } fi # as_fn_append # as_fn_arith ARG... # ------------------ # Perform arithmetic evaluation on the ARGs, and store the result in the # global $as_val. Take advantage of shells that can avoid forks. The arguments # must be portable across $(()) and expr. if (eval "test \$(( 1 + 1 )) = 2") 2>/dev/null then : eval 'as_fn_arith () { as_val=$(( $* )) }' else $as_nop as_fn_arith () { as_val=`expr "$@" || test $? -eq 1` } fi # as_fn_arith if expr a : '\(a\)' >/dev/null 2>&1 && test "X`expr 00001 : '.*\(...\)'`" = X001; then as_expr=expr else as_expr=false fi if (basename -- /) >/dev/null 2>&1 && test "X`basename -- / 2>&1`" = "X/"; then as_basename=basename else as_basename=false fi if (as_dir=`dirname -- /` && test "X$as_dir" = X/) >/dev/null 2>&1; then as_dirname=dirname else as_dirname=false fi as_me=`$as_basename -- "$0" || $as_expr X/"$0" : '.*/\([^/][^/]*\)/*$' \| \ X"$0" : 'X\(//\)$' \| \ X"$0" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X/"$0" | sed '/^.*\/\([^/][^/]*\)\/*$/{ s//\1/ q } /^X\/\(\/\/\)$/{ s//\1/ q } /^X\/\(\/\).*/{ s//\1/ q } s/.*/./; q'` # Avoid depending upon Character Ranges. as_cr_letters='abcdefghijklmnopqrstuvwxyz' as_cr_LETTERS='ABCDEFGHIJKLMNOPQRSTUVWXYZ' as_cr_Letters=$as_cr_letters$as_cr_LETTERS as_cr_digits='0123456789' as_cr_alnum=$as_cr_Letters$as_cr_digits # Determine whether it's possible to make 'echo' print without a newline. # These variables are no longer used directly by Autoconf, but are AC_SUBSTed # for compatibility with existing Makefiles. ECHO_C= ECHO_N= ECHO_T= case `echo -n x` in #((((( -n*) case `echo 'xy\c'` in *c*) ECHO_T=' ';; # ECHO_T is single tab character. xy) ECHO_C='\c';; *) echo `echo ksh88 bug on AIX 6.1` > /dev/null ECHO_T=' ';; esac;; *) ECHO_N='-n';; esac # For backward compatibility with old third-party macros, we provide # the shell variables $as_echo and $as_echo_n. New code should use # AS_ECHO(["message"]) and AS_ECHO_N(["message"]), respectively. as_echo='printf %s\n' as_echo_n='printf %s' rm -f conf$$ conf$$.exe conf$$.file if test -d conf$$.dir; then rm -f conf$$.dir/conf$$.file else rm -f conf$$.dir mkdir conf$$.dir 2>/dev/null fi if (echo >conf$$.file) 2>/dev/null; then if ln -s conf$$.file conf$$ 2>/dev/null; then as_ln_s='ln -s' # ... but there are two gotchas: # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail. # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable. # In both cases, we have to default to `cp -pR'. ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe || as_ln_s='cp -pR' elif ln conf$$.file conf$$ 2>/dev/null; then as_ln_s=ln else as_ln_s='cp -pR' fi else as_ln_s='cp -pR' fi rm -f conf$$ conf$$.exe conf$$.dir/conf$$.file conf$$.file rmdir conf$$.dir 2>/dev/null # as_fn_mkdir_p # ------------- # Create "$as_dir" as a directory, including parents if necessary. as_fn_mkdir_p () { case $as_dir in #( -*) as_dir=./$as_dir;; esac test -d "$as_dir" || eval $as_mkdir_p || { as_dirs= while :; do case $as_dir in #( *\'*) as_qdir=`printf "%s\n" "$as_dir" | sed "s/'/'\\\\\\\\''/g"`;; #'( *) as_qdir=$as_dir;; esac as_dirs="'$as_qdir' $as_dirs" as_dir=`$as_dirname -- "$as_dir" || $as_expr X"$as_dir" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$as_dir" : 'X\(//\)[^/]' \| \ X"$as_dir" : 'X\(//\)$' \| \ X"$as_dir" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X"$as_dir" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` test -d "$as_dir" && break done test -z "$as_dirs" || eval "mkdir $as_dirs" } || test -d "$as_dir" || as_fn_error $? "cannot create directory $as_dir" } # as_fn_mkdir_p if mkdir -p . 2>/dev/null; then as_mkdir_p='mkdir -p "$as_dir"' else test -d ./-p && rmdir ./-p as_mkdir_p=false fi # as_fn_executable_p FILE # ----------------------- # Test if FILE is an executable regular file. as_fn_executable_p () { test -f "$1" && test -x "$1" } # as_fn_executable_p as_test_x='test -x' as_executable_p=as_fn_executable_p # Sed expression to map a string onto a valid CPP name. as_tr_cpp="eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'" # Sed expression to map a string onto a valid variable name. as_tr_sh="eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'" exec 6>&1 ## ----------------------------------- ## ## Main body of $CONFIG_STATUS script. ## ## ----------------------------------- ## _ASEOF test $as_write_fail = 0 && chmod +x $CONFIG_STATUS || ac_write_fail=1 cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # Save the log message, to keep $0 and so on meaningful, and to # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" This file was extended by tsung $as_me 1.8.0, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES CONFIG_HEADERS = $CONFIG_HEADERS CONFIG_LINKS = $CONFIG_LINKS CONFIG_COMMANDS = $CONFIG_COMMANDS $ $0 $@ on `(hostname || uname -n) 2>/dev/null | sed 1q` " _ACEOF case $ac_config_files in *" "*) set x $ac_config_files; shift; ac_config_files=$*;; esac cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 # Files that config.status was made for. config_files="$ac_config_files" _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 ac_cs_usage="\ \`$as_me' instantiates files and other configuration actions from templates according to the current configuration. Unless the files and actions are specified as TAGs, all are instantiated by default. Usage: $0 [OPTION]... [TAG]... -h, --help print this help, then exit -V, --version print version number and configuration settings, then exit --config print configuration, then exit -q, --quiet, --silent do not print progress messages -d, --debug don't remove temporary files --recheck update $as_me by reconfiguring in the same conditions --file=FILE[:TEMPLATE] instantiate the configuration file FILE Configuration files: $config_files Report bugs to ." _ACEOF ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"` ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\''/g"` cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ tsung config.status 1.8.0 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" Copyright (C) 2021 Free Software Foundation, Inc. This config.status script is free software; the Free Software Foundation gives unlimited permission to copy, distribute and modify it." ac_pwd='$ac_pwd' srcdir='$srcdir' INSTALL='$INSTALL' test -n "\$AWK" || AWK=awk _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # The default lists apply if the user does not specify any file. ac_need_defaults=: while test $# != 0 do case $1 in --*=?*) ac_option=`expr "X$1" : 'X\([^=]*\)='` ac_optarg=`expr "X$1" : 'X[^=]*=\(.*\)'` ac_shift=: ;; --*=) ac_option=`expr "X$1" : 'X\([^=]*\)='` ac_optarg= ac_shift=: ;; *) ac_option=$1 ac_optarg=$2 ac_shift=shift ;; esac case $ac_option in # Handling of the options. -recheck | --recheck | --rechec | --reche | --rech | --rec | --re | --r) ac_cs_recheck=: ;; --version | --versio | --versi | --vers | --ver | --ve | --v | -V ) printf "%s\n" "$ac_cs_version"; exit ;; --config | --confi | --conf | --con | --co | --c ) printf "%s\n" "$ac_cs_config"; exit ;; --debug | --debu | --deb | --de | --d | -d ) debug=: ;; --file | --fil | --fi | --f ) $ac_shift case $ac_optarg in *\'*) ac_optarg=`printf "%s\n" "$ac_optarg" | sed "s/'/'\\\\\\\\''/g"` ;; '') as_fn_error $? "missing file argument" ;; esac as_fn_append CONFIG_FILES " '$ac_optarg'" ac_need_defaults=false;; --he | --h | --help | --hel | -h ) printf "%s\n" "$ac_cs_usage"; exit ;; -q | -quiet | --quiet | --quie | --qui | --qu | --q \ | -silent | --silent | --silen | --sile | --sil | --si | --s) ac_cs_silent=: ;; # This is an error. -*) as_fn_error $? "unrecognized option: \`$1' Try \`$0 --help' for more information." ;; *) as_fn_append ac_config_targets " $1" ac_need_defaults=false ;; esac shift done ac_configure_extra_args= if $ac_cs_silent; then exec 6>/dev/null ac_configure_extra_args="$ac_configure_extra_args --silent" fi _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 if \$ac_cs_recheck; then set X $SHELL '$0' $ac_configure_args \$ac_configure_extra_args --no-create --no-recursion shift \printf "%s\n" "running CONFIG_SHELL=$SHELL \$*" >&6 CONFIG_SHELL='$SHELL' export CONFIG_SHELL exec "\$@" fi _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 exec 5>>config.log { echo sed 'h;s/./-/g;s/^.../## /;s/...$/ ##/;p;x;p;x' <<_ASBOX ## Running $as_me. ## _ASBOX printf "%s\n" "$ac_log" } >&5 _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # Handling of arguments. for ac_config_target in $ac_config_targets do case $ac_config_target in "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;; "tsung.spec") CONFIG_FILES="$CONFIG_FILES tsung.spec" ;; "tsung.sh") CONFIG_FILES="$CONFIG_FILES tsung.sh" ;; "tsung-recorder.sh") CONFIG_FILES="$CONFIG_FILES tsung-recorder.sh" ;; "examples/*.xml") CONFIG_FILES="$CONFIG_FILES examples/*.xml" ;; "src/tsung_stats.pl") CONFIG_FILES="$CONFIG_FILES src/tsung_stats.pl" ;; "src/tsung-plotter/tsplot.py") CONFIG_FILES="$CONFIG_FILES src/tsung-plotter/tsplot.py" ;; "src/log2tsung.pl") CONFIG_FILES="$CONFIG_FILES src/log2tsung.pl" ;; "src/tsung_controller/tsung_controller.app") CONFIG_FILES="$CONFIG_FILES src/tsung_controller/tsung_controller.app" ;; "src/tsung_recorder/tsung_recorder.app") CONFIG_FILES="$CONFIG_FILES src/tsung_recorder/tsung_recorder.app" ;; "src/tsung/tsung.app") CONFIG_FILES="$CONFIG_FILES src/tsung/tsung.app" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; esac done # If the user did not use the arguments to specify the items to instantiate, # then the envvar interface is used. Set only those that are not. # We use the long form for the default assignment because of an extremely # bizarre bug on SunOS 4.1.3. if $ac_need_defaults; then test ${CONFIG_FILES+y} || CONFIG_FILES=$config_files fi # Have a temporary directory for convenience. Make it in the build tree # simply because there is no reason against having it here, and in addition, # creating and moving files from /tmp can sometimes cause problems. # Hook for its removal unless debugging. # Note that there is a small window in which the directory will not be cleaned: # after its creation but before its name has been assigned to `$tmp'. $debug || { tmp= ac_tmp= trap 'exit_status=$? : "${ac_tmp:=$tmp}" { test ! -d "$ac_tmp" || rm -fr "$ac_tmp"; } && exit $exit_status ' 0 trap 'as_fn_exit 1' 1 2 13 15 } # Create a (secure) tmp directory for tmp files. { tmp=`(umask 077 && mktemp -d "./confXXXXXX") 2>/dev/null` && test -d "$tmp" } || { tmp=./conf$$-$RANDOM (umask 077 && mkdir "$tmp") } || as_fn_error $? "cannot create a temporary directory in ." "$LINENO" 5 ac_tmp=$tmp # Set up the scripts for CONFIG_FILES section. # No need to generate them if there are no CONFIG_FILES. # This happens for instance with `./config.status config.h'. if test -n "$CONFIG_FILES"; then ac_cr=`echo X | tr X '\015'` # On cygwin, bash can eat \r inside `` if the user requested igncr. # But we know of no other shell where ac_cr would be empty at this # point, so we can use a bashism as a fallback. if test "x$ac_cr" = x; then eval ac_cr=\$\'\\r\' fi ac_cs_awk_cr=`$AWK 'BEGIN { print "a\rb" }' /dev/null` if test "$ac_cs_awk_cr" = "a${ac_cr}b"; then ac_cs_awk_cr='\\r' else ac_cs_awk_cr=$ac_cr fi echo 'BEGIN {' >"$ac_tmp/subs1.awk" && _ACEOF { echo "cat >conf$$subs.awk <<_ACEOF" && echo "$ac_subst_vars" | sed 's/.*/&!$&$ac_delim/' && echo "_ACEOF" } >conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 ac_delim_num=`echo "$ac_subst_vars" | grep -c '^'` ac_delim='%!_!# ' for ac_last_try in false false false false false :; do . ./conf$$subs.sh || as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 ac_delim_n=`sed -n "s/.*$ac_delim\$/X/p" conf$$subs.awk | grep -c X` if test $ac_delim_n = $ac_delim_num; then break elif $ac_last_try; then as_fn_error $? "could not make $CONFIG_STATUS" "$LINENO" 5 else ac_delim="$ac_delim!$ac_delim _$ac_delim!! " fi done rm -f conf$$subs.sh cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 cat >>"\$ac_tmp/subs1.awk" <<\\_ACAWK && _ACEOF sed -n ' h s/^/S["/; s/!.*/"]=/ p g s/^[^!]*!// :repl t repl s/'"$ac_delim"'$// t delim :nl h s/\(.\{148\}\)..*/\1/ t more1 s/["\\]/\\&/g; s/^/"/; s/$/\\n"\\/ p n b repl :more1 s/["\\]/\\&/g; s/^/"/; s/$/"\\/ p g s/.\{148\}// t nl :delim h s/\(.\{148\}\)..*/\1/ t more2 s/["\\]/\\&/g; s/^/"/; s/$/"/ p b :more2 s/["\\]/\\&/g; s/^/"/; s/$/"\\/ p g s/.\{148\}// t delim ' >$CONFIG_STATUS || ac_write_fail=1 rm -f conf$$subs.awk cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 _ACAWK cat >>"\$ac_tmp/subs1.awk" <<_ACAWK && for (key in S) S_is_set[key] = 1 FS = "" } { line = $ 0 nfields = split(line, field, "@") substed = 0 len = length(field[1]) for (i = 2; i < nfields; i++) { key = field[i] keylen = length(key) if (S_is_set[key]) { value = S[key] line = substr(line, 1, len) "" value "" substr(line, len + keylen + 3) len += length(value) + length(field[++i]) substed = 1 } else len += 1 + keylen } print line } _ACAWK _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 if sed "s/$ac_cr//" < /dev/null > /dev/null 2>&1; then sed "s/$ac_cr\$//; s/$ac_cr/$ac_cs_awk_cr/g" else cat fi < "$ac_tmp/subs1.awk" > "$ac_tmp/subs.awk" \ || as_fn_error $? "could not setup config files machinery" "$LINENO" 5 _ACEOF # VPATH may cause trouble with some makes, so we remove sole $(srcdir), # ${srcdir} and @srcdir@ entries from VPATH if srcdir is ".", strip leading and # trailing colons and then remove the whole line if VPATH becomes empty # (actually we leave an empty line to preserve line numbers). if test "x$srcdir" = x.; then ac_vpsub='/^[ ]*VPATH[ ]*=[ ]*/{ h s/// s/^/:/ s/[ ]*$/:/ s/:\$(srcdir):/:/g s/:\${srcdir}:/:/g s/:@srcdir@:/:/g s/^:*// s/:*$// x s/\(=[ ]*\).*/\1/ G s/\n// s/^[^=]*=[ ]*$// }' fi cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 fi # test -n "$CONFIG_FILES" eval set X " :F $CONFIG_FILES " shift for ac_tag do case $ac_tag in :[FHLC]) ac_mode=$ac_tag; continue;; esac case $ac_mode$ac_tag in :[FHL]*:*);; :L* | :C*:*) as_fn_error $? "invalid tag \`$ac_tag'" "$LINENO" 5;; :[FH]-) ac_tag=-:-;; :[FH]*) ac_tag=$ac_tag:$ac_tag.in;; esac ac_save_IFS=$IFS IFS=: set x $ac_tag IFS=$ac_save_IFS shift ac_file=$1 shift case $ac_mode in :L) ac_source=$1;; :[FH]) ac_file_inputs= for ac_f do case $ac_f in -) ac_f="$ac_tmp/stdin";; *) # Look for the file first in the build tree, then in the source tree # (if the path is not absolute). The absolute path cannot be DOS-style, # because $ac_f cannot contain `:'. test -f "$ac_f" || case $ac_f in [\\/$]*) false;; *) test -f "$srcdir/$ac_f" && ac_f="$srcdir/$ac_f";; esac || as_fn_error 1 "cannot find input file: \`$ac_f'" "$LINENO" 5;; esac case $ac_f in *\'*) ac_f=`printf "%s\n" "$ac_f" | sed "s/'/'\\\\\\\\''/g"`;; esac as_fn_append ac_file_inputs " '$ac_f'" done # Let's still pretend it is `configure' which instantiates (i.e., don't # use $as_me), people would be surprised to read: # /* config.h. Generated by config.status. */ configure_input='Generated from '` printf "%s\n" "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' `' by configure.' if test x"$ac_file" != x-; then configure_input="$ac_file. $configure_input" { printf "%s\n" "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 printf "%s\n" "$as_me: creating $ac_file" >&6;} fi # Neutralize special characters interpreted by sed in replacement strings. case $configure_input in #( *\&* | *\|* | *\\* ) ac_sed_conf_input=`printf "%s\n" "$configure_input" | sed 's/[\\\\&|]/\\\\&/g'`;; #( *) ac_sed_conf_input=$configure_input;; esac case $ac_tag in *:-:* | *:-) cat >"$ac_tmp/stdin" \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; esac ;; esac ac_dir=`$as_dirname -- "$ac_file" || $as_expr X"$ac_file" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$ac_file" : 'X\(//\)[^/]' \| \ X"$ac_file" : 'X\(//\)$' \| \ X"$ac_file" : 'X\(/\)' \| . 2>/dev/null || printf "%s\n" X"$ac_file" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q'` as_dir="$ac_dir"; as_fn_mkdir_p ac_builddir=. case "$ac_dir" in .) ac_dir_suffix= ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_dir_suffix=/`printf "%s\n" "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. ac_top_builddir_sub=`printf "%s\n" "$ac_dir_suffix" | sed 's|/[^\\/]*|/..|g;s|/||'` case $ac_top_builddir_sub in "") ac_top_builddir_sub=. ac_top_build_prefix= ;; *) ac_top_build_prefix=$ac_top_builddir_sub/ ;; esac ;; esac ac_abs_top_builddir=$ac_pwd ac_abs_builddir=$ac_pwd$ac_dir_suffix # for backward compatibility: ac_top_builddir=$ac_top_build_prefix case $srcdir in .) # We are building in place. ac_srcdir=. ac_top_srcdir=$ac_top_builddir_sub ac_abs_top_srcdir=$ac_pwd ;; [\\/]* | ?:[\\/]* ) # Absolute name. ac_srcdir=$srcdir$ac_dir_suffix; ac_top_srcdir=$srcdir ac_abs_top_srcdir=$srcdir ;; *) # Relative name. ac_srcdir=$ac_top_build_prefix$srcdir$ac_dir_suffix ac_top_srcdir=$ac_top_build_prefix$srcdir ac_abs_top_srcdir=$ac_pwd/$srcdir ;; esac ac_abs_srcdir=$ac_abs_top_srcdir$ac_dir_suffix case $ac_mode in :F) # # CONFIG_FILE # case $INSTALL in [\\/$]* | ?:[\\/]* ) ac_INSTALL=$INSTALL ;; *) ac_INSTALL=$ac_top_build_prefix$INSTALL ;; esac _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # If the template does not know about datarootdir, expand it. # FIXME: This hack should be removed a few years after 2.60. ac_datarootdir_hack=; ac_datarootdir_seen= ac_sed_dataroot=' /datarootdir/ { p q } /@datadir@/p /@docdir@/p /@infodir@/p /@localedir@/p /@mandir@/p' case `eval "sed -n \"\$ac_sed_dataroot\" $ac_file_inputs"` in *datarootdir*) ac_datarootdir_seen=yes;; *@datadir@*|*@docdir@*|*@infodir@*|*@localedir@*|*@mandir@*) { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 printf "%s\n" "$as_me: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&2;} _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_datarootdir_hack=' s&@datadir@&$datadir&g s&@docdir@&$docdir&g s&@infodir@&$infodir&g s&@localedir@&$localedir&g s&@mandir@&$mandir&g s&\\\${datarootdir}&$datarootdir&g' ;; esac _ACEOF # Neutralize VPATH when `$srcdir' = `.'. # Shell code in configure.ac might set extrasub. # FIXME: do we really want to maintain this feature? cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_sed_extra="$ac_vpsub $extrasub _ACEOF cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 :t /@[a-zA-Z_][a-zA-Z_0-9]*@/!b s|@configure_input@|$ac_sed_conf_input|;t t s&@top_builddir@&$ac_top_builddir_sub&;t t s&@top_build_prefix@&$ac_top_build_prefix&;t t s&@srcdir@&$ac_srcdir&;t t s&@abs_srcdir@&$ac_abs_srcdir&;t t s&@top_srcdir@&$ac_top_srcdir&;t t s&@abs_top_srcdir@&$ac_abs_top_srcdir&;t t s&@builddir@&$ac_builddir&;t t s&@abs_builddir@&$ac_abs_builddir&;t t s&@abs_top_builddir@&$ac_abs_top_builddir&;t t s&@INSTALL@&$ac_INSTALL&;t t $ac_datarootdir_hack " eval sed \"\$ac_sed_extra\" "$ac_file_inputs" | $AWK -f "$ac_tmp/subs.awk" \ >$ac_tmp/out || as_fn_error $? "could not create $ac_file" "$LINENO" 5 test -z "$ac_datarootdir_hack$ac_datarootdir_seen" && { ac_out=`sed -n '/\${datarootdir}/p' "$ac_tmp/out"`; test -n "$ac_out"; } && { ac_out=`sed -n '/^[ ]*datarootdir[ ]*:*=/p' \ "$ac_tmp/out"`; test -z "$ac_out"; } && { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \`datarootdir' which seems to be undefined. Please make sure it is defined" >&5 printf "%s\n" "$as_me: WARNING: $ac_file contains a reference to the variable \`datarootdir' which seems to be undefined. Please make sure it is defined" >&2;} rm -f "$ac_tmp/stdin" case $ac_file in -) cat "$ac_tmp/out" && rm -f "$ac_tmp/out";; *) rm -f "$ac_file" && mv "$ac_tmp/out" "$ac_file";; esac \ || as_fn_error $? "could not create $ac_file" "$LINENO" 5 ;; esac done # for ac_tag as_fn_exit 0 _ACEOF ac_clean_files=$ac_clean_files_save test $ac_write_fail = 0 || as_fn_error $? "write failure creating $CONFIG_STATUS" "$LINENO" 5 # configure is writing to config.log, and then calls config.status. # config.status does its own redirection, appending to config.log. # Unfortunately, on DOS this fails, as config.log is still kept open # by configure, so config.status won't be able to write to it; its # output is simply discarded. So we exec the FD to /dev/null, # effectively closing config.log, so it can be properly (re)opened and # appended to by config.status. When coming back to configure, we # need to make the FD available again. if test "$no_create" != yes; then ac_cs_success=: ac_config_status_args= test "$silent" = yes && ac_config_status_args="$ac_config_status_args --quiet" exec 5>/dev/null $SHELL $CONFIG_STATUS $ac_config_status_args || ac_cs_success=false exec 5>>config.log # Use ||, not &&, to avoid exiting from the if with $? = 1, which # would make configure fail if this is the last instruction. $ac_cs_success || as_fn_exit 1 fi if test -n "$ac_unrecognized_opts" && test "$enable_option_checking" != no; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 printf "%s\n" "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi tsung-1.8.0/CHANGELOG.md0000644000201100017670000011124714377756736014215 0ustar nniclausdream# Changelog # ## [1.8.0] - 2023-03-02 - Major enhancements and bugfixes ## ### Fixed ### - Fix ts_http:split_body for non-chunked responses [#302](https://github.com/processone/tsung/pull/302) - Fix: and does not work with lte and gte [#300](https://github.com/processone/tsung/pull/300) - Fix typos [#394](https://github.com/processone/tsung/pull/394) - Pubsub fixes [#290](https://github.com/processone/tsung/pull/290) - Change the wrong closing tag to the correct one [#384](https://github.com/processone/tsung/pull/384) - Fix(websocket): can connect now [#349](https://github.com/processone/tsung/pull/349) - Fix waiting for response body on HTTP HEAD requests with Content-Length set [#334](https://github.com/processone/tsung/pull/334) - Fix docs to substitute for-loop counter variable [#330](https://github.com/processone/tsung/pull/330) - MQTT subscribe packet MUST set qos to 1 for the fixed header [#327](https://github.com/processone/tsung/pull/327) - Tsung fails in ts_dynvars:merge when arguments are [<<>>, Dynvars] [#258](https://github.com/processone/tsung/pull/258) - Fix connection with websocket ssl [#310](https://github.com/processone/tsung/pull/310) - Fix ts_config_http:parse_URL/4 to deal with empty paths [#307](https://github.com/processone/tsung/pull/307) ### Changed ### - Update conf-sessions.rst [#390](https://github.com/processone/tsung/pull/390) - Improve docs: Add links to apache benchmark [#389](https://github.com/processone/tsung/pull/389) - Travis: Test against Erlang 21 [#323](https://github.com/processone/tsung/pull/323) - Reduce loglevel for xpath page parsing errors [#322](https://github.com/processone/tsung/pull/322) - Docs: fixed add_cookie example [#321](https://github.com/processone/tsung/pull/321) - Use --dygraph even when tsung_stats.pl is found in the path [#291](https://github.com/processone/tsung/pull/291) - Log HTTP Digest configuration on debug level [#299](https://github.com/processone/tsung/pull/299) - Make generated graphs more comparable over time and space. [#292](https://github.com/processone/tsung/pull/292) - Run value by ts_search:subst/2 [#311](https://github.com/processone/tsung/pull/311) - Handle ts_server_websocket_ssl:close/1 when socket is none [#325](https://github.com/processone/tsung/pull/325) - Substitution in [#317](https://github.com/processone/tsung/pull/317) ### Added ### - Add support for IP_BIND_ADDRESS_NO_PORT option [#400](https://github.com/processone/tsung/pull/400) - Add ping support [#289](https://github.com/processone/tsung/pull/289) - Add websocket_origin parameters to customize Origin header [#388](https://github.com/processone/tsung/pull/388) - Add local_file_server [#237](https://github.com/processone/tsung/pull/237) - Support RFC 7395 (WebSocket) framing [#397](https://github.com/processone/tsung/pull/397) - Add forwarding latency statistic for mqtt [#287](https://github.com/processone/tsung/pull/287) - Add option to disable SNI for TLS connections [#344](https://github.com/processone/tsung/pull/344) - Add PURGE for Varnish support [#326](https://github.com/processone/tsung/pull/326) - Mqtt connect's will topic with dynamic substitution [#273](https://github.com/processone/tsung/pull/273) - Add custom headers with websocket request [#296](https://github.com/processone/tsung/pull/296) - Add ClientId option on MQTT connect message [#303](https://github.com/processone/tsung/pull/303) ## [1.7.0] - 2017-08-28 - Major enhancements and bugfixes ## ### Fixed ### - [#117] Closing TCP connection in think state considered an error? - [#121] Unable to send a line feed in websoscket request - [#126] Timeouts for *get_next_session* not reported correctly - [#136] MAX_PROC not actually changeable - [#161] Websocket match randomly fail - [#162] ** not working along with **? - [#204] Timeout won't apply unless something happen to socket - [#218] Certificates are not getting set/sent correctly - [PR #148] clear accumulated data when websocket closed - [PR #183] fix formula for load average - [PR #202] fixing oauth 1.0 authorization header creation, signature with body, alphanumerical nonce - [PR #228] Where IQ PING was enabled from XMPP server. Tsung cannot reply ### Changed ### - [#136] Speedup Tsung starting when using hundreds of clients - [#145] Update or remove eldap in favor of OTP's eldap - [#150] Distributed Erlang Port Range Hard-Coded - [#159] Use new time API when building Tsung with otp R18 - [PR #124] Rename configure.in to configure.ac - [PR #125] Work around compiler warning and provide backward compatibility - [PR #198] Record extra headers in the HTTP proxy recorder - minimum erlang version is R16B ### Added ### - [#132] WSS connections - [#182] add option to set websocket subprotocols - [#189] Add direct-ip support for tsung nodes interconnection - [#201] add option to start a phase after all generated users in the previous phase have finished their session - [#225] stop entire test execution in do - [#242] tsung to support Linux client to using a range of secondary IP addresses - [PR #151] Add option to specify SSL protocol - [PR #153] Add latency measurement to XMPP MUC and PubSub - [PR #233] BOSH support for chunked transfer encoding. - [PR #235] Provide regex to use with varnish log - [PR #240] Added Jabber support for SASL EXTERNAL - add option to start tsung with only the web view (to view old runs stats) ## [1.6.0] - 2015-07-20 - Major enhancements and bugfixes ## ### Fixed ### - [TSUN-225] - SSL Session Caching Issues - [TSUN-292] - Indecipherable error with no arrivalphase elements - [TSUN-294] - Logging(?) of unmatched dyn vars puts lot of pressure on controller - [TSUN-295] - tsung status crashes test run - [TSUN-296] - Reported response size in dumpfile seems to be way too low - [TSUN-297] - Float values in thinktimes substitution - [TSUN-305] - Can't connect with +TLS to ejabberd/XMPP - [TSUN-308] - Handle *ssl_closed* in *ts_client* - [TSUN-309] - RNG Seeding is too weak and causes collisions - [TSUN-312] - Handle cast new beam failed - [TSUN-316] - LDAP scenarios fail when compiled with R16A/B due to *asn1rt_ber_bin* - [TSUN-320] - `get_os_data(freemem, {unix, linux})` crashes on Linux 3.10.0 - [PR #91] - [MQTT] Last Will and Testament should not be included if will_topic is not set. - [PR #104] - Fix problem with MQTT SUBACK packages - [PR #107] - Fix crash when `use_controller_vm="false"` and controller id is not empty - [PR #109] - Fix race condition in `ts_utils:make_dir/1` ### Changed ### - [TSUN-307] - Allow all HTTP headers to be overridden by - [PR #111] - Add subst for amqp exchange, routingKey and queue name - [PR #79] - added support for mqtt user and password - [PR #81] - Allow setting of dynamic vars for MQTT's username and password - [PR #93] - Add two config option ### Added ### - [TSUN-290] - Add a web dashboard embedded in tsung controller - [TSUN-293] - Enable node local dumptraffic log - [TSUN-304] - Add command line option to add additional erlang module load paths - [TSUN-306] - Add connection_timeout option - [PR #106] - XMPP message latency measurement ## [1.5.1] - 2014-04-07 - Major enhancements and bugfixes ## ### Fixed ### - [TSUN-250] - BOSH Crash - [TSUN-252] - Too many requests when using max_restart - [TSUN-253] - Code blocks in html version of user manual is unreadable - [TSUN-256] - Unexpected ack="global" behaviour on BOSH - [TSUN-265] - Wrong header line in tsung.dump - [TSUN-270] - Substitution not working in path attribute - [TSUN-271] - SSL does not work with erlang >= R16 - [TSUN-272] - Support literal IPv6 addresses when defining servers - [TSUN-278] - Tsung 1.5.0 is notable to do https out of the box when it is compiled from tarballs - [TSUN-279] - Tsung 1.5.0 is not able to do substitution of hostname or port in a URL. It only can do substitution of path - [TSUN-281] - Fix debian build for Tsung 1.5, replaces DocBook with Sphinx - [TSUN-285] - In some rare conditions in a distributed setup, Tsung fails to start the load test. - [TSUN-287] - request.max statistic lower than request.mean - [PR #71] - oAuth bug fix, PUT method - [PR #41] - Fix websocket path subst - [PR #44] - Add bidi attribute to change_type - [PR #49] - Fix websocket close issue: we should wait a close response ### Changed ### - [TSUN-255] - Fix unused vars in tq_amqp - [TSUN-259] - Tsung in Fedora - [TSUN-268] - Use xmerl_sax_parser:file/2 in case of xml parsing error - [TSUN-276] - Add text frame support for websocket - [TSUN-284] - Do not use boot files to start tsung and tsung_controller applications - [PR #51] - Updated dygraph charting library to the latest release - [PR #65] - AMQP: add multiple channel, add waitForConfirms and waitForMessages - [PR #70] - Add bosh_path config option - [PR #74] - Add text frame support for Websocket, and update doc ### Added ### - [TSUN-260] - Add option to change popularities of sessions for each phase - [TSUN-264] - New comparison operators - [TSUN-269] - Logging of request tags to dumpfile - [TSUN-275] - Add MQTT support - [TSUN-280] - Tsung to support pkcs#12 certificates or at least cacerts, clientcerts and keys - [PR #42] - Adding *all_except_body* option to *ts_http request* subst. - Adding mysqladmin monitoring options to erlang monitors. - Adding mean rate calculation to tsung_stats reports. - Adding *--title option* to set header of report - [PR #75] - Support SSL/TLS client certificate file attributes for jabber starttls ## [1.5.0] - 2013-05-24 - Major enhancements and bugfixes ## ### Fixed ### - [TSUN-208] - in the jabber plugin, substitutions for raw request doesn't work in some cases. - [TSUN-209] - If tag doesn't work with Tsung 1.4.2 - [TSUN-212] - Incorrect ERTS version being set on build. - [TSUN-215] - normal ack timeout shouldn't used for global ack - [TSUN-217] - If statement breaks on empty string - [TSUN-218] - Race condition in tsung-recorder - [TSUN-219] - Site fails to load via proxy recorder - [TSUN-220] - Large configuration files trigger error - [TSUN-229] - compatibility with erlang R15B - [TSUN-230] - can't connect with TLS + ejabberd - [TSUN-232] - Tsung for bosh protocol doesn't send a empty request to keep the user session alive. - [TSUN-234] - Error encoding json string with escape_uri - [TSUN-238] - Content-Length parsing broken - [TSUN-241] - Invalid link Other in the graph.html - [TSUN-245] - Message when dtd is not found not trivial ### Changed ### - [TSUN-174] - add an option to set resource in XMPP - [TSUN-222] - Support unsubscribe operation for Jabber pubsub module - [TSUN-228] - allow substitutions on cookies - [TSUN-236] - Add probability support for servers - [TSUN-242] - add timestamp and request duration in dump=protocol for http - [TSUN-246] - http PATCH support ### Added ### - [TSUN-214] - Ability to pass attributes for node creation for XMPP pubsub protocol. - [TSUN-227] - add new dynamic variable to get server hostname and port - [TSUN-231] - add option to use weights instead of probabilities for sessions - [TSUN-239] - add BOSH support - [TSUN-240] - add websocket support - [TSUN-244] - Percentile computation - [TSUN-248] - add AMQP support ## [1.4.2] - 2012-01-04 - Minor enhancements and bugfixes ## ### Fixed ### - [TSUN-199] - computation of NUsers is wrong - [TSUN-206] - build failure with erlang R15B ### Changed ### - [TSUN-202] - IPv6 support - [TSUN-203] - snmp oids should be customizable in the config file - [TSUN-205] - handle dyn_variables as array in test conditions (if/until/while) ### Added ### - [TSUN-191] - allow outputting log to stdout - [TSUN-192] - structured log output (JSON) - [TSUN-193] - accept configuration from stdin - [TSUN-197] - Have bug and error message on stderr and not stdout. ## [1.4.1] - 2011-09-13 - Minor bugfixes ## ### Fixed ### - [TSUN-188] - munin plugin is not working in 1.4.0 - [TSUN-189] - the controller VM is not used in some case - [TSUN-190] - pgsql recorder can record a connect request in an already connected session ## [1.4.0] - 2011-09-05 - Major enhancements and bugfixes ## ### Fixed ### - [TSUN-129] - regexp (defined in match or dynvars) can fail when chunk encoding is used. - [TSUN-150] - Munin monitoring broken by cpu stats config request - [TSUN-163] - Tsung doesn't detect subdomains. - [TSUN-166] - snmp monitoring does not work with erlang R14A - [TSUN-171] - maxnumber set in a phase is not always enforced - [TSUN-172] - auth sasl can't authenticate against ejabberd - [TSUN-178] - some characters can make url and headers rewriting fail in the recorder - [TSUN-179] - tsung generated message stanzas are not XMPP compliant - [TSUN-180] - file server crash if a dynamic substitution use it - [TSUN-182] - When many clients are configured with few static users, none of them are launched. - [TSUN-183] - tsung can stop too soon in some cases - [TSUN-184] - *random_number* with start and end actually returns a number from start+1 to end - [TSUN-187] - Client IP scan is very slow on Linux; also uses obsolete `ifconfig` ### Changed ### - [TSUN-54] - tsung is very slow when a lot of dynamic variables are set - [TSUN-96] - generating more than 64k connections from a single machine - [TSUN-106] - Add content-encoding support for dynvar extraction - [TSUN-123] - add option to read usernames from an exteraln file for jabber - [TSUN-125] - use the new re module everywhere instead of regexp/gregexp - [TSUN-152] - Add *state_rcv* record as parameter to *get_message* function. - [TSUN-153] - dynvar used in match may contain regexp special characters - [TSUN-185] - handle postgresql extended protocol ### Added ### - [TSUN-162] - add foreach tag (loop when a dyn_variable is a list) - [TSUN-164] - add a switch to allow light queries/replies logging - [TSUN-165] - add a way to synchronize users for all plugins. - [TSUN-167] - add do=dump option to matching - [TSUN-168] - thinktimes value could be dynamically generated with dyn_variable - [TSUN-181] - add option to simulate slow connections ## [1.3.3] - 2010-08-17 - Minor bugfixes ## ### Fixed ### - [TSUN-154] - parent proxy doesn't work anymore in 1.3.x (tested with 1.3.2 and 1.3.0). - [TSUN-155] - url substitution is broken in some cases - [TSUN-156] - Tsung not using sessions with low probabilities - [TSUN-157] - ssl doesn't work with erlang R14A - [TSUN-158] - failure when a proxy is used and an URL substitution is set - [TSUN-159] - HTTP cookies support is broken when a proxy is used - [TSUN-160] - tsung can sometimes hang at the beginning using distributed setup - [TSUN-161] - if statement not allowed in a transaction ## [1.3.2] - 2010-06-14 - Major bugfixes and enhancements ## ### Fixed ### - [TSUN-128] - Apostrophes cause string to convert to deep list in setdynvars with Erlang function. - [TSUN-130] - static users starting time is wrong - [TSUN-131] - tsung can stop too early when static users are used - [TSUN-132] - http cookies: accept domains equals to hostname with a leading "." - [TSUN-133] - proxy-recorder with SSL fails on large client packets (multiple TCP packets) - [TSUN-138] - when an error occurred( for ex a timeout during a request) and a client exits, started transactions are not updated - [TSUN-140] - tsung does not honor the Proxy-Connection: keep-Alive or Connection: keep-Alive header if the proxy is HTTP/1.0 - [TSUN-142] - http recorder can fail with https rewriting and chunked encoding - [TSUN-147] - UDP & bidi does not seem to work - [TSUN-148] - dynvar not found when used in match - [TSUN-149] - tsung doesn't work with Amazon Elastic load balancing - [TSUN-151] - tsung_stats.pl produces invalid `*.gplot` files ### Changed ### - [TSUN-82] - XMPP vhost support - [TSUN-127] - add ability tu use floats for thinktimes - [TSUN-139] - option to set random seed for launchers. - [TSUN-141] - add dynamic variable support for postgres - [TSUN-146] - New tsplot yfactor configuration support breaks most common configurations ### Added ### - [TSUN-135] - add support for SASL ANONYMOUS and PLAIN authentication for XMPP - [TSUN-136] - add new plugin distributed testing of filesystem (nfs for ex), using a generic mode for executing erlang functions on clients nodes - [TSUN-137] - add a way to mix requests types inside a single session - [TSUN-143] - global time limit for the load test - [TSUN-145] - tsung should support json parsing for dynamic variable ## [1.3.1] - 2009-09-09 - Major bugfixes and enhancements ## ### Fixed ### - [TSUN-92] - the computation of the minimum for sample_counter is wrong - [TSUN-93] - maxnumber not respected if several clients are used - [TSUN-102] - dyn_variable configuration fails if variable name is not a valid erlang atom - [TSUN-103] - Network error handling in munin plugin - [TSUN-104] - tsung-plotter can't handle the os_mon statistics - [TSUN-108] - Cannot handle complicated dyn_var name - [TSUN-109] - Tsung status displays always phase one even if you have more than one phase - [TSUN-110] - Cookie header not present if the URL is dynamically generated by a previous redirection (302) - [TSUN-117] - Bug in HTTP: empty header can be generated in some case - [TSUN-118] - HTTPS proxy recorder: ts_utils:to_https incorrectly handles Content-Length for POST requests - [TSUN-119] - tsung can crash when reading empty values from a csv file - [TSUN-122] - same http cookie key with different domains don't work ### Changed ### - [TSUN-47] - ts_mon can be a bottleneck during very high load testing - [TSUN-77] - Structural requests or goto-like action for match in HTTP - [TSUN-81] - Dynamic variables API - [TSUN-83] - file_server using fixed tuple instead of list - [TSUN-85] - external entity should be copied into the log directory of a run. - [TSUN-87] - add dynamic code evaluation in set_dynvars - [TSUN-88] - add mkactivity method support in webdav - [TSUN-91] - reduce memory consumption by hibernating client process while in think state - [TSUN-97] - disable smp erlang for client beam for performance reason - [TSUN-98] - try several times to connect to the server before aborting a session - [TSUN-99] - make substitution work in - [TSUN-100] - improve scalability of ts_launcher - [TSUN-105] - Add load average statistic to server monitoring - [TSUN-111] - add option to manually add a cookie in http requests - [TSUN-113] - split tsung command into two separate tsung and tsung-recorder commands - [TSUN-116] - add ability to run several tsung running in parallel on the same hosts - [TSUN-120] - Https recorder: Remove "Secure" from "Set-Cookie" header. ### Added ### - [TSUN-25] - add a way to start sessions in a specific order at specified times - [TSUN-89] - include tsung-plotter into the tsung distribution - [TSUN-90] - add support for monitoring server cpu/mem using munin-node - [TSUN-94] - add log action for match - [TSUN-95] - add a default dyn_variable with a unique tsung_userid - [TSUN-107] - add MUC support to the jabber doc/plugin - [TSUN-114] - add option to apply function to data before looking for a match - [TSUN-115] - add pubsub support to the jabber plugin ## [1.3.0] - 2008-09-03 - Major bugfixes and enhancements ## ### Fixed ### - [TSUN-30] - SNMP monitoring gives an error - [TSUN-57] - using -l with a relative path make distributed load fails with timeout error - [TSUN-60] - https recorder broken if an HTML document includes absolute urls - [TSUN-67] - Typo breaks recording of if-modified-since headers - [TSUN-68] - some sites doesn't work with ":443" added in the "Host" header with https - [TSUN-71] - Tsung does not work with R12B (httpd_util funs removed) - [TSUN-73] - Wrong parsing HTTP multipart/form-data in http request - POST form doesn't work - [TSUN-75] - can not define more -pa arguments - [TSUN-84] - dyn variables that don't match should be set to an empty string ### Changed ### - [TSUN-40] - problem to rewrite url for https with gzip-encoded html. - [TSUN-48] - tcp/udp buffer size should be customizable in the XML config file. - [TSUN-59] - if a User-Agent header is set in
, it should override the global one. - [TSUN-62] - add ability to loop back to a previous request in a session - [TSUN-63] - check for ssl and crypto application at compile time - [TSUN-65] - enhance dynamic variables. - [TSUN-66] - add global mean and counter computation and reporting for samples - [TSUN-69] - add option to read content of a POST request from an external file - [TSUN-79] - setting 'Host' header with http_header doesn't work as expected ### Added ### - [TSUN-56] - ldap plugin - [TSUN-58] - add a new statistics backend to dump all stats in a file - [TSUN-61] - add a Webdav plugin - [TSUN-64] - add md5 authentication in the pgsql plugin - [TSUN-72] - Add support for defining dyn_variables using XPath - [TSUN-78] - mysql plugin - [TSUN-80] - add random thinktime with in a given range ( [min,max]) - [TSUN-76] - add explanation for errors name in the documentation ### [1.2.2] - 2008-02-23 - Minor bugfixes and enhancements ### ### Fixed ### - [TSUN-30] - SNMP monitoring gives an error - [TSUN-31] - dyn_variable usage - [TSUN-35] - udp is not working - [TSUN-36] - default regexp for dyn_variable doesn't work in all case - [TSUN-38] - server monitoring crash if an ethernet interface's name is more than 6 chars long - [TSUN-39] - https recording doesn't work with most browsers - [TSUN-43] - session should not terminate if rosterjid is not defined - [TSUN-49] - doesn't work with jabber plugin - [TSUN-51] - tsung does not work with R12B (httpd_util funs removed) - [TSUN-53] - postgresql errors not reported in all cases - [TSUN-55] - no error counter when userid_max is reached ### Changed ### - [TSUN-14] - no_ack messages and asynchronous msg sent by the server are not available in the reports - [TSUN-27] - handle bidirectional protocols - [TSUN-28] - Refactoring needed to ease the change of the userid / password generation code - [TSUN-29] - Multiple file_server support - [TSUN-32] - make snmp server options tunable - [TSUN-34] - add custom http headers - [TSUN-44] - tsung should ignore whitespace keepalive from xmpp server - [TSUN-45] - add kernel-poll support for better performance - [TSUN-46] - add number of open connections in statistics - [TSUN-47] - ts_mon can be a bottleneck during very high load testing - [TSUN-50] - use the whole range of Id (from 0 to userid_max) before reusing already used Ids ### Added ### - [TSUN-26] - Ability to loop on a given sequence of phase - [TSUN-52] - Adding comment during script capture - [TSUN-41] - add support for parent proxy for http only (not https) ## [1.2.1] - 2006-10-07 - Minor bugfixes and enhancements ## ### Fixed ### - [TSUN-5] get traffic from all interfaces instead of only *eth0* in erlang os monitoring (Linux) - [TSUN-18 the pgsql recorder fails if the client doesn't try first an SSL connection - [TSUN-19] a % character in some requests (eg. type=sql for pgsql) make the config_server crash. - [TSUN-20] pgsql client fails while parsing data from server - [TSUN-21] substitution in URL is not working properly when a new server or port is set - [TSUN-23] set default http version (1.1) - [TSUN-24] destination=previous doesn't work (jabber) ### Changed ### - [TSUN-15] listen port is now customizable with the command line - [TSUN-17] add option to setup postgresql server IP and port at runtime for the recorder - [TSUN-22] add support for PUT, DELETE and HEAD methods for http ## [1.2.0] - 2006-05-29 - Major feature enhancements ## ### Fixed ### - fix beams communication problem introduced in new erlang releases. - fix several small problems with 'use_controller_vm' option - fix regression in recorder for WWW-Authentication (anders.nygren@gmail.com) - fix presence:roster request - fix online: must use presence:initial to switch to online status - fix single user agent case. - minor fixes for HTTP parsing ### Changed ### - change name: idx-tsunami is now called tsung - import snmp_mgr src from R9C2 to enable SNMP with R10B - rebuild boot scripts if erlang version is different from compile time - many DTD improvements - improved match: add loop|abort|restart on (no)match behavior, multiple match tags is now possible (suggested by msmith@truelink.com) - ip is no more mandatory (default is 0.0.0.0) - close client socket when connection:closed is ask by the server (this should enable https recording with IE) - roster enhancements (jasonwtucker@gmail.com) ### Added ### - add new plugin: pgsql for postgresql load testing - new: it's now possible to set multiple servers (selected at runtime by round robin) - add size_rcv stats - freemem and packet stats for Solaris (jasonwtucker@gmail.com) - clients and monitoring can use hosts list defined in environment variables, for use with batch schedulers (openpbs/torque, LSF and OAR) - performance improvements in stats engine for very high load (use session_cache) - add plugin architecture in recorder; add pgsql plugin - add *presence:directed* , *presence:broadcast* & *presence:final* requests for jabber (jasonwtucker@gmail.com) - sip-digest authentication (jasonwtucker@gmail.com) - add pubsub support (mickael.remond@process-one.net) ## [1.1.0] - 2005-09-05 - Major feature enhancements ## ### Added ### - new feature: HTTP proxy load testing in now possible (set *http_use_server_as_proxy* to true) - add dynamic substitution support for jabber - add 'raw' type of msg for Jabber (use the new 'data' attribute) - UserAgent is now customizable for HTTP testing ### Changed ### - add the dynamic variable list to dynamic substitutions - Add an option to run all components (controller and launcher) within a single erlang beam (*use_controller_vm*). Should ease idx-tsunami use for light load tests - internal: Host header is now set during configuration phase ### Fixed ### - fix bash script for solaris (jasonwtucker@gmail.com) - fix: several 'idx-tsunami status' can be run simultaneously (reported by Adam Spotton) - fix last phase duration - fix recorder: must log absolute url if only the scheme has changed ## [1.0.3] - 2005-07-08 - Minor bugfixes ## ### Fixed ### - fix broken https recording Thx to johann.messner@jku.at for bug reporting : - fix: forgot to add *"?"* when an URL is absolute and had a query part - fix regression in the recorder (introduced in 1.0.2): must use CAPS for method, wrong content-length in recorder causing POST requests to silently fail - fix Host: header when port is != 80 ### Added ### - add *ts_file_server* module ### Changed ### - allow multiple 'dyn_variable' in DTD ## [1.0.2] - 2005-06-06 - Minor bugfixes ## ### Fixed ### - fix: the recorder is working now with R10B: replace call to *httpd_parse:request_header* in recorder by an internal func (the func was removed in R10B) - update configure scripts (should build on RHEL3/x86_64) ### Changed ### - remote beam startup is now tunable (-r ssh/rsh) - internal changes in ts_os_mon (suggested by R. Lenglet) ## [1.0.1] - 2004-11-18 - Major bugfixes ## ### Fixed ### - fix: broken free mem on non linux arch (Matthew Schulkind) - small fixes to the DTD Thx to Jonathan Bresler for testing and bug reporting : - fix: broken 'global', 'local' and 'no_ack' requests and size computation - fix: broken ids in jabber messages - fix: broken online/offline in user_server - default thinktime can now be overridden ### Added ### - add script to convert apache log file (combined) to idx-tsunami XML ### Changed ### - improved configure: add *--with-erlang* option and xmerl PATH detection idx-tsunami now compiles both with R9C and R10B - many improvements/fixes in analyse_msg.pl ## [1.0] - 2004-08-13 - Minor bugfixes ## ### Fixed ### - fix: broken path when building debian package - fix add_dynparams for jabber ### Added ### - add rpm target in makefile - implement status - add 'match' in graph and doc ## [1.0.beta7] - 2004-07-20 - Minor bugfixes ## ### Fixed ### - HTTP: really (?) fix parsing of no content-length with connection:close - better handling of configure (--prefix is working) - fix: ssl_ciphers option is working again ### Changed ### - add different types of output backend (currently, only 'text' works; 'rrdtool' is started but unfinished) ## [1.0.beta6] - 2004-05-05 - Minor feature enhancements ## ### Added ### - add a DTD for the configuration file - add dynamic request substitution (mickael.remond@erlang-fr) - add dynamic variable parsing from response (can be used later in the session for request substitution) - add response pattern to match (log if not match) ### Fixed ### - HTTP: fix partial header parsing (mickael.remond@erlang-fr.org) - HTTP: fix chunk parsing when the chunk-size is split across two packets - HTTP: fix parsing of no content-length with connection:close case - fix: do not connect in init anymore; this fix too long phases when connection time is high. ### Changed ### - check for bad input (config file, name) - merge client and client_rcv processes into a single process - connect stat is now for both new connections and reconnections - check phase duration in launcher - various code cleanup ## [1.0.beta5] - 2004-03-25 - Major Feature enhancements ## ### Added ### - add SNMP monitoring (not yet customizable) - SOAP Support: IDX-Tsunami can now record and replay SOAP HTTP scenario. The SOAPAction HTTP header is now recorded ### Fixed ### - fix remote start: log filename is now encoded to avoid bad parsing of log_file by 'erl' - Added ~/.idx-tsunami creation in idx-tsunami script if the directory does not already exist - HTTP: fix Cookie support: Cookie are not necessarily separated by "; " - HTTP: fix long POST request in the recorder: dorecord message was missing enclosing curly brackets, and the body length counter were mistakenly taking the header size in its total ### Changed ### - Extension of XML attribute entity normalisation - HTTP: Content-type support in the recorder (needed to handle non-HTML form encoded posts) - add autoconf support to detect Erlang installation path - Preliminary Windows support: A workaround has been introduced in the code to handle behaviour difference between Erlang Un*x and Erlang Windows on how the command-line is handled. When an assumption is made on the string type of a parameter, it should be check that this is actually a string and not an atom. ## [1.0.beta4] - 2004-03-16 - Minor bugfixes ## ### Fixed ### - fix lost cookie when transfer-encoding:chunked is used - fix config parsing (the last request of the last page of a session was not marked as endpage) - don't crash anymore on error during start or stop ## [1.0.beta3] - 2004-02-24 - Minor feature enhancements ## ### Fixed ### - fix stupid bug in start script for recorder - HTTP: fix '&' writes in the XML recorder for 'content' attribute ### Changed ### - HTTP: enhanced Cookies parsing ('domain' and 'path' implemented). - ssl_ciphers can be customized - change log directory structure: all log files in one directory per test - change stats names: page_resptime -> page, response_time -> request ### Added ### - add HTML reports (requires the perl Template toolkit) ## [1.0.beta2] - 2004-02-11 - Minor feature enhancements ## ### Changed ### - reorganise the sources - add tools to build a debian package - fix documentations - add minimalistic man page - syntax change: GETIMS +date replace by GET +'if_modified_since' ## [1.0.beta1] - 2005-02-03 - Major Feature Enhancements ## ### Added ### - rewrite the configuration engine. Now use an XML file. - add recording application: use as a HTTP proxy to record session into XML format - add support to OS monitoring (cpu, memory, network). Currently, use an erlang agent on the remote nodes; SNMP is on the TODO list. (mickael.remond@erlang-fr.org) - can now use several IPs per client host - several arrival phases can be set with different arrival rates and duration - can set test duration instead of number of users - add user defined statistics using a 'transaction' tag ### Fixed ### - HTTP: fix cookies and POST handling (mickael.remond@erlang-fr.org) - HTTP: rewrite the parser (faster and cleaner) - fix bad timeout computation when close occur for persistent client - bugfixes and other enhancements. - fix memory leak with ssl (half-closed connections) ## [0.2.1] - 2003-12-09 - Minor bugfixes and small enhancements ## ### Changed ### - optimize session memory consumption: use an ets table to store session setup - HTTP: preliminary chunked-encoding support in HTTP/1.1 - HTTP: Absolute URL are handled (server and port can be overridden ) - no more .hosts.erlang required - add stats on simultaneous users ### Fixed ### - HTTP: fix crash when content-length is not set in headers - HTTP: fix POST method ## [0.2.0] - 2003-08-29 - Major Feature Enhancements ## ### Added ### - add 'realtime' stats - add new 'parse' type of protocol - add reconnection support (persistent client) - add basic HTTP and HTTPS support - split the application in two parts: a single controller (tsunami_controller), and the clients (tsunami) - switch to R9C ## [0.1.1] - 2002-08-13 - Bugfix release ## ### Fixed ### - fix config file - fix few typos in docs - fix init script - few optimizations in user_server.erl - switch to R8B ## [0.1.0] - 2001-05-30 - Initial release ## [Unreleased]: https://github.com/processone/tsung/compare/v1.6.0...HEAD [1.6.0]: https://github.com/processone/tsung/compare/v1.5.1...v1.6.0 [1.5.1]: https://github.com/processone/tsung/compare/v1.5.0...v1.5.1 [1.5.0]: https://github.com/processone/tsung/compare/v1.4.2...v1.5.0 [1.4.2]: https://github.com/processone/tsung/compare/v1.4.1...v1.4.2 [1.4.1]: https://github.com/processone/tsung/compare/v1.4.0...v1.4.1 [1.4.0]: https://github.com/processone/tsung/compare/v1.3.3...v1.4.0 [1.3.3]: https://github.com/processone/tsung/compare/v1.3.2...v1.3.3 [1.3.2]: https://github.com/processone/tsung/compare/v1.3.1...v1.3.2 [1.3.1]: https://github.com/processone/tsung/compare/v1.3.0...v1.3.1 [1.3.0]: https://github.com/processone/tsung/compare/v1.2.2...v1.3.0 [1.2.2]: https://github.com/processone/tsung/compare/v1.2.1...v1.2.2 [1.2.1]: https://github.com/processone/tsung/compare/v1.2.0...v1.2.1 [1.2.0]: https://github.com/processone/tsung/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/processone/tsung/compare/v1.0.2...v1.1.0 [1.0.2]: https://github.com/processone/tsung/compare/v1.0.1...v1.0.2 [1.0.1]: https://github.com/processone/tsung/compare/v0.2.1...v1.0.1 [0.2.1]: https://github.com/processone/tsung/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/processone/tsung/compare/v0.1.1...v0.2.0 [0.1.1]: https://github.com/processone/tsung/compare/v0.1.0...v0.1.1 [PR #148]: https://github.com/processone/tsung/pull/148 [PR #183]: https://github.com/processone/tsung/pull/183 [PR #202]: https://github.com/processone/tsung/pull/202 [PR #228]: https://github.com/processone/tsung/pull/228 [PR #124]: https://github.com/processone/tsung/pull/124 [PR #125]: https://github.com/processone/tsung/pull/125 [PR #198]: https://github.com/processone/tsung/pull/198 [PR #151]: https://github.com/processone/tsung/pull/151 [PR #153]: https://github.com/processone/tsung/pull/153 [PR #233]: https://github.com/processone/tsung/pull/233 [PR #235]: https://github.com/processone/tsung/pull/235 [PR #240]: https://github.com/processone/tsung/pull/240 [PR #91]: https://github.com/processone/tsung/pull/91 [PR #104]: https://github.com/processone/tsung/pull/104 [PR #107]: https://github.com/processone/tsung/pull/107 [PR #109]: https://github.com/processone/tsung/pull/109 [PR #111]: https://github.com/processone/tsung/pull/111 [PR #79]: https://github.com/processone/tsung/pull/79 [PR #81]: https://github.com/processone/tsung/pull/81 [PR #93]: https://github.com/processone/tsung/pull/93 [PR #106]: https://github.com/processone/tsung/pull/106 [PR #71]: https://github.com/processone/tsung/pull/71 [PR #41]: https://github.com/processone/tsung/pull/41 [PR #44]: https://github.com/processone/tsung/pull/44 [PR #49]: https://github.com/processone/tsung/pull/49 [PR #51]: https://github.com/processone/tsung/pull/51 [PR #65]: https://github.com/processone/tsung/pull/65 [PR #70]: https://github.com/processone/tsung/pull/70 [PR #74]: https://github.com/processone/tsung/pull/74 [PR #42]: https://github.com/processone/tsung/pull/42 [PR #75]: https://github.com/processone/tsung/pull/75 [#117]: https://github.com/processone/tsung/issues/117 [#121]: https://github.com/processone/tsung/issues/121 [#126]: https://github.com/processone/tsung/issues/126 [#136]: https://github.com/processone/tsung/issues/136 [#161]: https://github.com/processone/tsung/issues/161 [#162]: https://github.com/processone/tsung/issues/162 [#204]: https://github.com/processone/tsung/issues/204 [#218]: https://github.com/processone/tsung/issues/218 [#136]: https://github.com/processone/tsung/issues/136 [#145]: https://github.com/processone/tsung/issues/145 [#150]: https://github.com/processone/tsung/issues/150 [#159]: https://github.com/processone/tsung/issues/159 [#132]: https://github.com/processone/tsung/issues/132 [#182]: https://github.com/processone/tsung/issues/182 [#189]: https://github.com/processone/tsung/issues/189 [#201]: https://github.com/processone/tsung/issues/201 [#225]: https://github.com/processone/tsung/issues/225 [#242]: https://github.com/processone/tsung/issues/242 tsung-1.8.0/CONTRIBUTORS0000644000201100017670000000305714377756736014263 0ustar nniclausdream$Id$ AUTHOR: ====================== o Nicolas Niclausse : Maintainer; CONTRIBUTORS: ====================== o Jean François Lecomte : several enhancements for Jabber o Mickaël Rémond : erlang server monitoring; various patches for HTTP; configure support; SOAP support; initial dynamic substitution implementation. o Jérome Sautret: Multiple file patch for file_server, custom header for HTTP, bug reports o Jason Tucker: Solaris testing and fixes, jabber patches (sip_digest, roster and presence enhancements, bidi support for presence:subscribe ). o Pablo Polvorin: LDAP plugin, set_dynvars, xpath search for html, for/repeat loop, dynvars_api, hibernate. PubSub and MUC support. o Gregoire Reboul: MySQL plugin. o Dimitri Fontaine: SNMP & postgresql testing, patch for snmp, tsung-plotter o Oleg Nitz: Fix for Cookies over https, fix rewrite of POST (http recorder) o David Jez: allow substitutions in match o Will Brant: load info for monitoring, fix for tsplot o Jonathan Bresler: Jabber testing and bug reporting o Gordon Guthrie: tips for ssh setup on Suse o Romain Lenglet: Suggestions for ts_os_mon o Johann Messner: Bug reports o Anders Nygren: Documentation updates/suggestions, fix for recorder o Adam Spotton: Bug reports and tests (status, HTTP proxy load testing) o Matthew Schulkind: small fix to freemem computation o t ty: plugin tutorial o Jesper Wilhelmsson: testing New contributors since the migration to git are available here: https://github.com/processone/tsung/graphs/contributors tsung-1.8.0/debian/0000755000201100017670000000000014377757020013603 5ustar nniclausdreamtsung-1.8.0/debian/rules0000755000201100017670000000225514377756736014704 0ustar nniclausdream#!/usr/bin/make -f # Sample debian/rules that uses debhelper. # GNU copyright 1997 to 1999 by Joey Hess. # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 configure: configure-stamp configure-stamp: dh_testdir # Add here commands to configure the package. ./configure touch configure-stamp build: build-stamp build-stamp: configure-stamp dh_testdir # Add here commands to compile the package. $(MAKE) $(MAKE) -C docs singlehtml touch build-stamp clean: dh_testdir dh_testroot rm -f build-stamp configure-stamp # Add here commands to clean up after the build process. -$(MAKE) clean dh_clean install: build dh_testdir dh_testroot #dh_clean -k dh_installdirs # Add here commands to install the package into debian/tsung make install DESTDIR=$(CURDIR)/debian/tsung # Build architecture-independent files here. binary-indep: build install # We have nothing to do by default. dh_testdir dh_testroot dh_installdocs dh_installchangelogs dh_link dh_strip dh_compress dh_fixperms # dh_makeshlibs dh_installdeb # dh_perl dh_shlibdeps dh_gencontrol dh_md5sums dh_builddeb binary: binary-indep .PHONY: build clean binary-indep install configure tsung-1.8.0/debian/tsung.dirs0000644000201100017670000000006114377756736015640 0ustar nniclausdreamusr/bin/ usr/lib/tsung usr/share/tsung/templates tsung-1.8.0/debian/docs0000644000201100017670000000010014377756736014462 0ustar nniclausdreamCHANGELOG.md docs/_build/singlehtml CONTRIBUTORS README.md TODO tsung-1.8.0/debian/copyright0000644000201100017670000000143114377756736015552 0ustar nniclausdreamThis package was debianized by Nicolas Niclausse on Tue, 10 Feb 2004 12:09:23 +0100. It was downloaded from http://tsung.erlang-projects.org/ Upstream Author(s): Nicolas Niclausse Copyright: Tsung is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Tsung is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. See /usr/share/common-licenses/GPL tsung-1.8.0/debian/compat0000644000201100017670000000000214377756736015016 0ustar nniclausdream9 tsung-1.8.0/debian/control0000644000201100017670000000203614377756736015224 0ustar nniclausdreamSource: tsung Section: net Priority: optional Maintainer: Nicolas Niclausse Build-Depends: debhelper (>= 4.0.0), erlang-nox (>= 10.b.5-1) , python-sphinx, erlang-src, erlang-dev, autoconf Standards-Version: 3.6.0 Package: tsung Architecture: all Depends: erlang-nox (>= 10.b.5-1) Recommends: gnuplot, perl, ssh, libtemplate-perl, python-matplotlib Description: A distributed multi-protocol load testing tool. Tsung is a distributed load testing tool. It is protocol-independent and can currently be used to stress and benchmark HTTP, Jabber/XMPP, LDAP, MySQL and PostgreSQL servers. It simulates user behaviour using an XML description file, reports many measurements in real time (statistics can be customized with transactions, and graphics generated using gnuplot). For HTTP, it supports 1.0 and 1.1, has a proxy mode to record sessions, supports GET and POST methods, Cookies, and Basic WWW-authentication. It also has support for SSL. . More information is available at http://tsung.erlang-projects.org/ . tsung-1.8.0/debian/changelog0000644000201100017670000001162014377756736015472 0ustar nniclausdreamtsung (1.8.0-1) unstable; urgency=low * 1.8.0 -- Nicolas Niclausse Wed, 2 Mar 2023 00:09:05 +0100 tsung (1.7.0-1) unstable; urgency=low * 1.7.0 -- Nicolas Niclausse Tue, 28 Aug 2017 15:53:05 +0200 tsung (1.6.0-1) unstable; urgency=low * 1.6.0 -- Nicolas Niclausse Mon, 20 Jul 2015 09:53:05 +0200 tsung (1.5.1-1) unstable; urgency=low * 1.5.1 -- Nicolas Niclausse Wed, 09 Apr 2014 08:53:05 +0200 tsung (1.5.1a-1) unstable; urgency=low * fix make deb * prepare for new release -- Nicolas Niclausse Thu, 20 Feb 2014 10:53:05 +0200 tsung (1.5.0-1.2+nmu) unstable; urgency=low * Adding 'all_except_body' option to ts_http request subst. ** Using " will run substitutions on everything except body contents. * Adding 'mysqladmin' monitoring options to erlang monitors. ** Collects statistics on threads/questions on a mysql server. -- Don Kjer Wed, 31 Jul 2013 22:21:32 +0000 tsung (1.5.0-1.1+nmu) unstable; urgency=low * Fixing issue with attempting to set_opts on closed socket * Updating mochiweb for better xpath support * Adding mean rate calculation to tsung_stats reports. * Adding --title option to set header of report * Applying debian spelling correction patch -- Don Kjer Tue, 23 Jul 2013 06:00:53 +0000 tsung (1.5.0-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Fri, 24 May 2013 09:53:05 +0200 tsung (1.4.2-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Tue,4 Jan 2012 10:53:05 +0200 tsung (1.4.1-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Tue, 13 Sep 2011 10:53:05 +0200 tsung (1.4.0-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Mon, 5 Sep 2011 10:58:05 +0200 tsung (1.4.0a-1) unstable; urgency=low * working on 1.4.0 -- Nicolas Niclausse Tue, 26 Apr 2011 13:11:05 +0200 tsung (1.3.3-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Wed, 17 Aug 2010 18:11:05 +0200 tsung (1.3.2-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Wed, 14 Jun 2010 20:11:05 +0200 tsung (1.3.1-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Wed, 09 Aug 2009 08:11:05 +0200 tsung (1.3.0-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Wed, 03 Sep 2008 07:11:05 +0200 tsung (1.2.2-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Sat, 23 Feb 2008 08:11:05 +0200 tsung (1.2.1-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Wed, 20 Sep 2006 08:11:05 +0200 tsung (1.2.0-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Mon, 29 May 2006 09:11:05 +0200 tsung (1.1.0-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Mon, 6 Sep 2005 09:11:05 +0200 tsung (1.0.3-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Mon, 8 Jul 2005 17:11:05 +0200 tsung (1.0.2-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Mon, 6 Jun 2005 17:34:05 +0200 tsung (1.0.1-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Thu, 18 Nov 2004 08:34:05 +0200 tsung (1.0-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Thu, 12 Aug 2004 13:34:05 +0200 tsung (1.0.beta7-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Tue, 20 Jul 2004 18:57:01 +0200 tsung (1.0.beta6-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Tue, 4 May 2004 13:07:17 +0200 tsung (1.0.beta5-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Thu, 25 Mar 2004 17:41:25 +0100 tsung (1.0.beta4-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Tue, 16 Mar 2004 15:23:43 +0100 tsung (1.0.beta3-1) unstable; urgency=low * New upstream release -- Nicolas Niclausse Tue, 24 Feb 2004 19:06:03 +0100 tsung (1.0.beta2-1) unstable; urgency=low * Initial Release. -- Nicolas Niclausse Tue, 10 Feb 2004 12:09:23 +0100 tsung-1.8.0/vsn.mk0000644000201100017670000000000614377756736013531 0ustar nniclausdream1.8.0 tsung-1.8.0/tsung.sh.in0000755000201100017670000002014214377756736014501 0ustar nniclausdream#!/usr/bin/env bash UNAME=`uname` case $UNAME in "Linux") HOST=`hostname -s 2>/dev/null` RET=$? if [ $RET != 0 ]; then HOST=`hostname` echo "WARN: hostname -s failed, use '$HOST' as hostname" > /dev/stderr fi ;; "SunOS") HOST=`hostname`;; *) HOST=`hostname -s`;; esac INSTALL_DIR=@EXPANDED_LIBDIR@/tsung ERL=@ERL@ MAIN_DIR=$HOME/.tsung LOG_DIR=$MAIN_DIR/log LOG_OPT="log_dir \"$LOG_DIR/\"" MON_FILE="mon_file \"tsung.log\"" VERSION=@PACKAGE_VERSION@ NAMETYPE="-sname" PROTO_DIST=" -proto_dist inet_tcp " LISTEN_PORT=8090 USE_PARENT_PROXY=false PGSQL_SERVER_IP=127.0.0.1 PGSQL_SERVER_PORT=5432 NAME=tsung CONTROLLER=tsung_controller CONTROLLER_EXTENDS="" SMP_DISABLE=true WARM_TIME=1 MAX_PROCESS=250000 # start an embedded web dashboard (on port 8091) WEB_GUI=true # don't stop controller: let the GUI alive after the load is finished: KEEP_WEB_GUI=false EXCLUDE_TAG_LIST="" TSUNGPATH=$INSTALL_DIR/tsung-$VERSION/ebin CONTROLLERPATH=$INSTALL_DIR/tsung_controller-$VERSION/ebin EXTRA_LOAD_PATHS="" CONF_OPT_FILE="$HOME/.tsung/tsung.xml" DEBUG_LEVEL=5 ERL_RSH=" -rsh ssh " ERL_DIST_PORTS_MIN=64000 ERL_DIST_PORTS_MAX=65500 ERL_OPTS=" -smp auto +A 8 +K true @ERL_OPTS@ " COOKIE='tsung' SSL_CACHE="ts_ssl_session_cache" # 10 mn ssl session lifetime instead of 24h SSL_SESSION_LIFETIME="600" stop() { $ERL $ERL_OPTS $ERL_RSH -noshell $PROTO_DIST $NAMETYPE killer -setcookie $COOKIE $EXTRA_LOAD_PATHS -pa $TSUNGPATH -pa $CONTROLLERPATH -s tsung_controller stop_all $HOST -s init stop } view() { echo "Starting Tsung web only on port 8091" $ERL $ERL_OPTS $ERL_RSH -noshell $PROTO_DIST $NAMETYPE $CONTROLLER$CONTROLLER_EXTENDS -setcookie $COOKIE \ +P $MAX_PROCESS \ -kernel inet_dist_listen_min $ERL_DIST_PORTS_MIN -kernel inet_dist_listen_max $ERL_DIST_PORTS_MAX \ -s ts_web \ $EXTRA_LOAD_PATHS \ -pa $TSUNGPATH -pa $CONTROLLERPATH \ -sasl sasl_error_logger false \ -stdlib $LOG_OPT } start() { echo "Starting Tsung" $ERL $ERL_OPTS $ERL_RSH -noshell $PROTO_DIST $NAMETYPE $CONTROLLER$CONTROLLER_EXTENDS -setcookie $COOKIE \ +P $MAX_PROCESS \ -kernel inet_dist_listen_min $ERL_DIST_PORTS_MIN -kernel inet_dist_listen_max $ERL_DIST_PORTS_MAX \ -s tsung_controller \ $EXTRA_LOAD_PATHS \ -pa $TSUNGPATH -pa $CONTROLLERPATH \ -ssl session_cb $SSL_CACHE \ -ssl session_lifetime $SSL_SESSION_LIFETIME \ -sasl sasl_error_logger false \ -tsung_controller web_gui $WEB_GUI \ -tsung_controller keep_web_gui $KEEP_WEB_GUI \ -tsung_controller smp_disable $SMP_DISABLE \ -tsung_controller debug_level $DEBUG_LEVEL \ -tsung_controller warm_time $WARM_TIME \ -tsung_controller exclude_tag \"$EXCLUDE_TAG_LIST\" \ -tsung_controller config_file \"$CONF_OPT_FILE\" -tsung_controller $LOG_OPT -tsung_controller $MON_FILE } debug() { $ERL $ERL_OPTS $ERL_RSH $NAMETYPE $CONTROLLER$CONTROLLER_EXTENDS $PROTO_DIST -setcookie $COOKIE \ +P $MAX_PROCESS \ -kernel inet_dist_listen_min $ERL_DIST_PORTS_MIN -kernel inet_dist_listen_max $ERL_DIST_PORTS_MAX \ -s tsung_controller \ $EXTRA_LOAD_PATHS \ -pa $TSUNGPATH -pa $CONTROLLERPATH \ -ssl session_cb $SSL_CACHE \ -ssl session_lifetime $SSL_SESSION_LIFETIME \ -sasl sasl_error_logger \{file\,\"$LOG_DIR/tsung-sasl.log\"\} \ -tsung_controller web_gui $WEB_GUI \ -tsung_controller keep_web_gui $KEEP_WEB_GUI \ -tsung_controller warm_time $WARM_TIME \ -tsung_controller config_file \"$CONF_OPT_FILE\" \ -tsung_controller exclude_tag \"$EXCLUDE_TAG_LIST\" \ -tsung_controller $LOG_OPT -tsung_controller $MON_FILE } version() { echo "Tsung version $VERSION" exit 0 } checkconfig() { if [ ! -e $CONF_OPT_FILE ] && [ $CONF_OPT_FILE != "-" ] then echo "Config file $CONF_OPT_FILE doesn't exist, aborting !" exit 1 fi } maindir() { if [ ! -d $MAIN_DIR ] then echo "Creating local Tsung directory $MAIN_DIR" mkdir $MAIN_DIR fi } logdir() { if [ ! -d $LOG_DIR ] then echo "Creating Tsung log directory $LOG_DIR" mkdir $LOG_DIR fi } status() { SNAME=tsung_status_$RANDOM $ERL -noshell $NAMETYPE $SNAME -setcookie $COOKIE $EXTRA_LOAD_PATHS -pa $TSUNGPATH -pa $CONTROLLERPATH -s tsung_controller status $HOST -s init stop } checkrunning_controller() { RES=`status` if [ "$RES" != "Tsung is not started" ]; then echo "Tsung is already running, exit." exit 1 fi } usage() { prog=`basename $0` echo "Usage: $prog start|stop|debug|status|view" echo "Options:" echo " -f set configuration file (default is ~/.tsung/tsung.xml)" echo " (use - for standard input)" echo " -l set log directory where YYYYMMDD-HHMM dirs are created (default is ~/.tsung/log/)" echo " -i set controller id (default is empty)" echo " -r set remote connector (default is ssh)" echo " -s enable erlang smp on client nodes" echo " -p set maximum erlang processes per vm (default is 250000)" echo " -X add additional erlang load paths (multiple -X arguments allowed)" echo " -m write monitoring output on this file (default is tsung.log)" echo " (use - for standard output)" echo " -F use long names (FQDN) for erlang nodes" echo " -I use IP (FQDN) for erlang nodes; you can assign local bind available IP (not assigned; default is the host's name)" echo " -L SSL session lifetime (600sec by default)" echo " -w warmup delay (default is 1 sec)" echo " -n disable web GUI (started by default on port 8091)" echo " -k keep web GUI (and controller) alive after the test has finished" echo " -v print version information and exit" echo " -6 use IPv6 for Tsung internal communications" echo " -x list of requests tag to be excluded from the run (separated by comma)" echo " -t erlang inet listening TCP port min (default: 64000)" echo " -T erlang inet listening TCP port max (default: 65500)" echo " -h display this help and exit" exit } while getopts "6vhknf:l:d:r:i:Fsw:m:p:x:X:t:T:I:" Option do case $Option in f) CONF_OPT_FILE=$OPTARG;; l) # must add absolute path echo "$OPTARG" | grep -q "^/" RES=$? if [ "$RES" == 0 ]; then LOG_DIR=$OPTARG LOG_OPT="log_dir \"$OPTARG/\" " else LOG_DIR=$OPTARG LOG_OPT="log_dir \"$PWD/$OPTARG/\" " fi ;; m) MON_FILE="mon_file \"$OPTARG\"";; n) WEB_GUI="false";; k) KEEP_WEB_GUI="true";; d) DEBUG_LEVEL=$OPTARG;; p) MAX_PROCESS=$OPTARG;; X) EXTRA_LOAD_PATHS="$EXTRA_LOAD_PATHS -pa $OPTARG";; r) ERL_RSH=" -rsh $OPTARG ";; 6) PROTO_DIST=" -proto_dist inet6_tcp ";; F) NAMETYPE="-name";; L) SSL_SESSION_LIFETIME=$OPTARG;; w) WARM_TIME=$OPTARG;; t) ERL_DIST_PORTS_MIN=$OPTARG;; T) ERL_DIST_PORTS_MAX=$OPTARG;; s) SMP_DISABLE="false";; v) version;; i) ID=$OPTARG COOKIE=$COOKIE"_"$ID CONTROLLER=$CONTROLLER"_"$ID ;; x) EXCLUDE_TAG_LIST=$OPTARG;; I) NAMETYPE="-name" SERVER_IP=$OPTARG if [ "$SERVER_IP" != "" ]; then CONTROLLER_EXTENDS="@$SERVER_IP" fi ;; h) usage;; *) usage ;; esac done shift $(($OPTIND - 1)) case $1 in view) maindir logdir view ;; start) checkconfig maindir logdir start ;; debug) checkconfig maindir logdir debug ;; stop) stop ;; status) status ;; *) usage $0 ;; esac tsung-1.8.0/examples/0000755000201100017670000000000014377757020014177 5ustar nniclausdreamtsung-1.8.0/examples/websocket.xml.in0000644000201100017670000000237214377756736017335 0ustar nniclausdream {"user":"user", "password":"password"} ok {"uid":"%%_uid%%", "data":"data"} {"key":"value"} tsung-1.8.0/examples/thinks.xml.in0000644000201100017670000000227614377756736016652 0ustar nniclausdream tsung-1.8.0/examples/thinks2.xml.in0000644000201100017670000000226414377756736016731 0ustar nniclausdream tsung-1.8.0/examples/raw.xml.in0000644000201100017670000000174014377756736016136 0ustar nniclausdream tsung-1.8.0/examples/pgsql.xml.in0000644000201100017670000000344214377756736016474 0ustar nniclausdream SELECT * from accounts; SELECT * from users; tsung-1.8.0/examples/mysql.xml.in0000644000201100017670000000216514377756736016514 0ustar nniclausdream SHOW TABLES SELECT * FROM gens SELECT * FROM te tsung-1.8.0/examples/mqtt.xml.in0000644000201100017670000000363414377756736016336 0ustar nniclausdream test_message tsung-1.8.0/examples/ldap.xml.in0000644000201100017670000000470314377756736016267 0ustar nniclausdream organizationalPerson inetOrgPerson person %%_new_user_cn%% fffs SomeSN some@mail.com tsung-1.8.0/examples/jabber.xml.in0000644000201100017670000001017214377756736016571 0ustar nniclausdream tsung-1.8.0/examples/jabber_starttls.xml.in0000644000201100017670000000527114377756736020535 0ustar nniclausdream tsung-1.8.0/examples/jabber_roster.xml.in0000644000201100017670000000530314377756736020167 0ustar nniclausdream tsung-1.8.0/examples/jabber_register.xml.in0000644000201100017670000000245514377756736020502 0ustar nniclausdream error tsung-1.8.0/examples/jabber_privacy.xml.in0000644000201100017670000000317614377756736020334 0ustar nniclausdream tsung-1.8.0/examples/jabber_node.xml.in0000644000201100017670000000676514377756736017613 0ustar nniclausdream tsung-1.8.0/examples/jabber_muc.xml.in0000644000201100017670000000607514377756736017444 0ustar nniclausdream tsung-1.8.0/examples/http_tag.xml.in0000644000201100017670000000241114377756736017153 0ustar nniclausdream tsung-1.8.0/examples/http_simple.xml.in0000644000201100017670000000424014377756736017673 0ustar nniclausdream tsung-1.8.0/examples/http_setdynvars.xml.in0000644000201100017670000000564614377756736020617 0ustar nniclausdream tsung-1.8.0/examples/http-oauth.xml.in0000644000201100017670000000472314377756736017446 0ustar nniclausdream tsung-1.8.0/examples/http_distributed.xml.in0000644000201100017670000001435714377756736020736 0ustar nniclausdream tsung-1.8.0/examples/http-digest.xml.in0000644000201100017670000000414214377756736017600 0ustar nniclausdream tsung-1.8.0/examples/fs-nfs.xml.in0000644000201100017670000000736314377756736016550 0ustar nniclausdream tsung-1.8.0/examples/bosh.xml.in0000644000201100017670000000503414377756736016300 0ustar nniclausdream tsung-1.8.0/examples/amqp.xml.in0000644000201100017670000001075614377756736016312 0ustar nniclausdream tsung-1.8.0/TODO0000644000201100017670000000006114377756736013063 0ustar nniclausdreamSee https://support.process-one.net/browse/TSUN tsung-1.8.0/LISEZMOI0000644000201100017670000000540614377756736013561 0ustar nniclausdream# $Id$ Tsung LISEZMOI 1. Introduction 1.1. Gnralits Ce document donne un rapide descriptifs de Tsung, qui est distribu sous les termes de la GNU General Public License version 2 (voir le fichier COPYING). 1.2. Qu'est-ce que ce logiciel fait? Le propos de Tsung est de simuler des utilisateurs afin de tester la monte en charge et les performances d'applications client/serveur (bases sur IP). Actuellement, les protocoles HTTP, Jabber, PostgreSQL, WEBDAV et LDAP sont implments, et Tsung est trs facilement extensible (voir le fichier doc/Design.txt pour une description de l'implmentation et des possibilits d'extensions). Tsung utilise le langage Erlang. Ce logiciel est capable de simuler plusieurs milliers d'utilisateurs simultanment, et ceux-ci peuvent tre rpartis sur plusieurs machines. Plus de 10000 utilisateurs peuvent tre simuls sur une seule machine; la limite suprieure dpend du type de hardware et galement de l'activit des clients simuls. L'ide est de simuler le comportement d'un client rel en utilisant un modle de type stochastique, ceci afin de reproduire le trafic plus fidlement que peuvent le faire de simple modles dterministes. Un utilisateur est caractris par une une suite d'actions (requetes, thinktime) faites au cours d'une session. Plusieurs sessions peuvent tre dfinies, chacune avec une popularit donne. De cette faon, lors de l'injection, chaque nouvel utilisateur utilisera un type de session en tirant alatoirement une session (en fonction de la popularit de chaque session). Un paramtre important est le l'inter-arrive des clients qui dtermine le taux d'arrive des clients sur le systme (ie. le nombre de clients arrivant sur le systme -- dmarrant leur session -- par unit de temps). Plusieurs phases peuvent tre dfinies pour un tests, chaque phase injectant des utilisateurs un taux donn. Dans l'implmentation actuelle, la taux d'arrive des clients et le temps entre message d'un mme client ("think time") sont modliss par une distribution exponentielle (par consquent, le processus d'arrive est un processus de Poisson). Voir galement le site http://tsung.erlang-projects.org/ Un manuel utilisateur est disponible en anglais: http://tsung.erlang-projects.org/user_manual.html 2. Installation & Configuration cf. http://tsung.erlang-projects.org/user_manual.html 2.3. Problmes/Bugs Envoyez vos questions/rapports la liste de diffusion https://lists.process-one.net/mailman/listinfo/tsung-users ou directement l'auteur, 2.4. Portabilit Ce logiciel a t test sous Linux, Solaris, FreeBSD. Il devrait fonctionner sous toute plate-forme support par Erlang. tsung-1.8.0/README.md0000644000201100017670000000147714377756736013666 0ustar nniclausdream# Tsung README [![Build Status](https://travis-ci.org/processone/tsung.svg?branch=master)](https://travis-ci.org/processone/tsung) ## Introduction This document gives pointers for information on this package which is distributed under the GNU General Public License version 2 (see file COPYING). ## What This Package Is Tsung is multi-protocol distributed load testing tool. It can be used to test the scalability and performances of IP based client/server applications (supported protocols: HTTP, WebDAV, SOAP, PostgreSQL, MySQL, LDAP, MQTT, AMQP and Jabber/XMPP) A User's manual is available : http://tsung.erlang-projects.org/user_manual/ ## Problems/Bugs Join the mailing-list: https://lists.process-one.net/mailman/listinfo/tsung-users or use the tracker https://github.com/processone/tsung/issues tsung-1.8.0/COPYING0000644000201100017670000004325414377756736013441 0ustar nniclausdream GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. tsung-1.8.0/tsung-1.0.dtd0000644000201100017670000003501614377756736014534 0ustar nniclausdream tsung-1.8.0/docs/0000755000201100017670000000000014377757020013311 5ustar nniclausdreamtsung-1.8.0/docs/images/0000755000201100017670000000000014377757020014556 5ustar nniclausdreamtsung-1.8.0/docs/images/tsung-report.png0000644000201100017670000024314614377756736017764 0ustar nniclausdreamPNG  IHDRb IDATxe\MgFADT 뵻^[TLnEPPB}?qw"q<fggfgݽYHTvB|F!BFR @!+0A!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!BƉV @EK֭BwX rB!d0A!q(!B B'rB!d3VDIW[BbD9m5Jٽ!BAsgN;ͯRQs!TtO|!JHҊ`2ȿQkuy{y!PфrBEkubXvIf}8&\ɚeL[*i:m92FPR, ž0kQ)_2 P6S6tUeeOW8|tՕ4>I\az]2>e(.w9CԄ*f&l8-bW_Hת uK/*!ʉmq[ŜplR|E6fWflf^R<,bcRۑu:wTJNgR {|蕡O4̧BDswY)@S:,T7U.)̥ɬ2š)dd3HdMg?S4EL9k2MUi^U64V,}&Q0ıdc +$c2IkimM VIw: Pd QΌ$$hw+[uITx.MK 9`@M) `[c7|NPȨ39Pd QQvtf7RùiSocS{g*dIQujڭSw#S>9irRL02+C0"+W2Kx5oLpńu|eۍW2*t9]Q'b%lQ4.]8Ǔ jO'l* @䲄+_LIdYWd9Pd X[>^6A󫩵ȤeY/F*h8]!-IƦ,Hdɤ5|*hbZKB('q> ӉÐTH=^.c6e #-Iyca|,'1 * Gõ|^uoƕ[xcQׄKJT әrlX?!d$47Ι] A8 mf6 )Co/՟!H/ԋ'XL ͻc>7&s*WaA! NQ<?*Q$]P3(ǻ]˛aϾRKT*Ss,9}YpN@FqJP.(Sd0Vs'"* F텐Bua{UIzX&\ {fՊ#R-`!Org$dϦpZn'!] >餞K9ki9${8PHg5'" oX2o;㩐~(brYBS' Atk;$~3 ,7#Tz=9_ cLGy/@( sUY?5+LxGHW:|F`U.1ė\`W΄,M*s  JKYrB!]X i#-~^ѹi.cxnғ+~ki1?,AH_o _47#T$Yc\{ZTi1YΩӗ;eqZ{1kpyJ <4 }lxd?SdnMvtf"qJǩYn-ܖ T$0eB*6,J1k95҉o)_ 6eL#=nHRux(=u4`@;^Lݡ+onB!0N.Gv+B!>c4B!>F!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!B B'Za7nB!Q\|)?,,, !Ajjj~wB!d0A!q(!BƩD^n9/i[Mا2:Oc!|穤Fj5ƍ[}/L(:ۘF3?Q‹$$L˷RI;##*R8n&9*Ԋ᭖0*j-*tag<[z] J.rQG+XJB2ƾz8Ib $o5oGS{/NDSMڭ}״- ruoܷK@hgf Rqg6FyB䱫y%+\wDP'ܗ:0zS-mmU_}-:Vw+v;bMIA@Mf!9 9ɰvg$+UCJnͩKNŒ rFЯf4By]B2]K,#2Ҍe}KjLp[ƺ3xɏIilߗKS%yUaR@!ϤPs*t޿vg`*Vv=鱚$c|e:vn~ԚqT[&.}tP+AY:VFژhH{j?uf=jL_y٨Nj\q+%DJb[yÅ#T([1EJ/yץ?:)| \7R);!fD$ yֽKRvJjI;}2ҹ;TΈ )JjaFMoNrifG6UiՍ om+P}L1u;]hzG_&!/K_O2UƖCOމApsZ5*g[XLPيfɾ'eۜMƧ*X8::gU5}ki]PP_sZE,ʑJ1U+2TY*v9PS<jO^J(S{_T$ҞY7b? 8Z(1\sJ^`8 Di#%/Zd^ڳj}G+pn`9ӫ,>~yaP/4;[)^!M6ZqKPۛ \{tTFf%~y/=QmyGB8<שqRp̭C7tsFD\ƭqj4xs')К3~.fy4ץ YbXGυ#Tt(g eoJ(l3=QnV8r8 ´91 ׺&Pׯ ˜}]Mi]eu*VgLeOqZ0#).Ɲ)_N@6<]Y:^d[3nSߙ(gMʊE.yH$p9ܗҞMuY06?$q_=AxxwǣfNlymNp/}:Ӯ+2 SvJyyx,JF:j?W_#Tt(ƹrϯ*{̈́k42SƷgbIsI]6n諹l&~iiwEyE@PO8[c,m;ʴ%vuE.pL̦Ud n8[nWit;'<=p31#%) T8BEmp{Fn;\*TDiT~^}&2QNg ár '\KNjNAY"~bJYFj(*^ǵǮ zud\ؽ-#c²zsU%wy7w7+kFWJ_n6i]U߄rͧQeT`\KKM]Jq51}mتL<д[Eϻ ЬLeKz&_|י9PQBȘx;󑛁laW[ReG;(St[ ]}Fz|s|?|(& *Y\]]'jG9_ R&ZďF1wߖr8n?ɷf}a*tkuf|Je*gU{\-s+\I{*(GeoRv3Z6gnݭ_wu2]j\^f{У;:wլ[c݀!KNҷE!*:s΅h^m) O~;G?3)eT’.JL߭%M7:X^-LbQI\[Əvٟrd_5:wwﻶ2lv7iTeHrfG)+;k7ڟxWϔXum]]`H]Hx*G ]Xt[4κ^MȭAX5ظ}j!`wljCtҎrnC9Xy.h@Z>xn7]Bƣ?~muOTڨ ;(c`&튦b`#P``ĝcě٣V9wG~q'žf~o$ e/)Wf%&@-Zc;wn>y1 fGHY T1{i^9TU㈈.=Ȋr̽ 輀*W"sIog=B!T&;NG=s+rxУ= \|.B޻ :r[7k7Irm;^|.p^fW*22P|XLSSnl<7R5Xĝ.,8FYpuuuuuv@ [xz-<ݳw_~5xTLEy cB7u$F@@dfǮeiqr41K;4s,pҭv2.떓"[n_on?8=Z(eZ488B /ˋZjϦ60EQ"ͯR ;,wR w~ؽ?DH~LIdjS9AUZ[~Pc78̣jvzVʶؼd}gdv/$O XBBB)zB?.I>?и?G"C"XAEņʠs-5Ni=jne&&vܹ);?|Tf)Sk_WI{~+U BgVtϥ:"Tby雋kvCtߜ"S,Uu&F ][E^ď dĴޛq/;8ЮĦ ~ފCx58㘫.[=FZ[v̻'Cn=nS'&+D1; T)5k]Jv>qU7VS°M~W7n7FZTgG=kJY}7W0[JSóo=<oخ_;-/VC8rT^2mM:wo߬շ2?;4kK*}jg[Ilscws{^ǞT#J3B eOlRjݶ݇ =jҼgqu[wo^^ya)}ݫ Cw76R IDATI)Ilo(2bwN#^JΣt5=tyɓ7P4=~ sΜs.uVXka8rHJJ[TYlCOjݺïkO%nvAC==U2:Qf-"?(U?GZ71˾7x)SsPa7Ĥ6=\:lQ;3z 6/?Bw(?hV\q1M773`3s䩣*SnV1`ն}B~eE *Ԭe@%KnU=iӳӨbA+gow 薥GGrMw ^ PH{ J|qXGدOZ֒_o~Ǥ@F@;iO&7cID*j Z:8uuRXC A@q-=cQZŮe5d+Yu>[r}6沜-ʷ!p,xy- R|:xw {A>lUg #]{կ;\ Z^k)GҼwъ_7V9gNp7gm87 =G5@/QFIs/4 sTHTgw"90@g2H?? (f ^x?h}Bk8hip[8G|RR!T%r71PFeaa~c>z mTԦׯ֜:3p㔍]楼\)(U6Q\ d~rw_ J(~R8gu!jR>ī>ܻ SG5=vl7a/@PhM TgU|8Q-֣x4C ~DL\@4#V}aVZ);ʆ^0q@IyBp$ q02 GY+eI+D3&@7+(HH :hsբ)uoaT*ݞAYL)Oj?Oll"JIs2SUth~ Y%LcA] ^z OmUopJl`$ZnqQq9.~,Z? }>Ok2A)ԮMw^-f b6 RSSsΔ;y'{D+5ԬhOӫaE/̝`5]CgG|@z-{YJǜZ5h:JY\|lYnrN\ ?0PξM@BzQGnj1|,mzC)jf.cO6MRԮviikCzO?[1lʂqN_#a޲ϒM Hٱ537,tsq\ެvGUW^vye|\)Xdy6eQC.=j&TJVollUv{In Ç`ը$,Wԭ;mƆ"~\%~U} 師a0A#XMC*fl,C{TReܜrꚒQ((rH$ݺ*XD9y"xΡRVW:U2Z%ht[gIv2v *sy((.օsK;[׬|rJ+TP6{ dsvqǖztҶc E15 ^a/-?_5V]QiE P#]Q}NzhlЍPIPrA bܛr=Y> ZiѬS {@ivTf}*uS{C4a`+w#rP>jЪVphbk"T݇9vvpd v>Ҹf#oejܛt|Vo=ץ:O!>##z^;~&7. ۦ~wn̥Z-{8̍MyMW9.+LaP*g-xYsK SnSGؗK-{[q+C&oe ;ѽ=^l9(UT7!g[m~բVo]O{})Lmk$i۠ձ7<~C7N$LV/ZJ7$W,+IRe00FA2u"uV#_\KOq?7{'+çLٕ/XE LKOReoDTrʞ*`. 4} LRc{+0Ag77=::rToxj sXؕo> uä ߤfz)qWOZS'=حZa.MVQC30IBH9 A_rP3aO <ɐ(?Lk@$_HHsA+={~eˏ/nzU/c3~ΥNVx/]6m,N4|-IZ!_2t{U8408IUc3I* Gs_c>rQ*he~ HR%lscreqŨvqmzZ,~c T-;`Ffs~KG@`3Q0dجt !T4Jpn_y*+;wnW{,3, @IY t*ԤY:N}AҎc5n) {1sO.hZ^am ݿZM5CY}{pN o,o7g v/Aa̗ PObo\]|ϱKoC&4S]W^2}Y ϯvq@ţXR^+ֵaK.?xbC<ƯmY[` 3(_+w+ zkH`k ۭϲvTᬀH/rOGU<_{LX]M)wLG<1Q߂:\/oܧN7/S꒭t{Xw~ИfxA8u.BNݥ5;nG% P1y|`>C|s ZX_E-]tرUZ4_/yb-;&eœoG-.}mmt)Lx=ݿu'l&T T|q̩|?~}~Uɂ9&I@UZnt$i77..#+mT&qIO.?}٩}mեsk/oc<2~[vݎJuM:U+BBbB!P QB!F9!2N$9[\g]/&ևPl>5}Q0;h6M!:w[v-&pw0ZDN'3N[[٫8Doܐ_5#`8C8I)kOzDf@;{ uչ)V+$U1߫q~B?|}}`Æ &&&3kjQ b%Q~y_P@jbA }Sx73]`>&36~ 4$atdVĻ YKYgB=L*rV˖-Gչ,VDa@+kTr!B(˗/B۷۴icx3/H3!Ո^24(ũʏbV°V(ݘ*ɣƢ:1a2i񌊽mA_ "y*VE 37fE%fʇ)!PIA//<+άm~??<)H#GY@zcu4U,=]̡l˗"YZgӿ;7MTJ% _30ho>VqJ(s=Rv沢&)*W{=HC9hM6-ZP}ٳgzr֢US}7j;%K0܏RF~N{yI)fC+*u9%X5 37A!T")=k8BrhjViw# Q}vk[I.zPME-:~…ׯ_8p t? \w%I33T"no#X1$GH5P)@(mG"ly-Xg JtVrBdoev_@*ǖqi5/=O|UB<_N Տ#J 2} ҢDL3y-3ZtQRO 2UHrE?+bA_oo;J9)gژ:[f+Rv_ ;9itwm_#~p,@Bd̗2K7SSJu\>f5k\X` =Y%yOv+P܀Aܪ 7~֓C/s}Cǟ¤"}]VwV__f7D+ȷRCB\ߨ4K;]!T~nh7Z8BLSF@!G!a7sc. [ۅ8^#K1v {]ה@JE_A*Lͩ@$Gi]?aŐ-l^/M<Ɩf8 [1=E^|΍]-ݮ}۽|,E0s͒:6lQQAś'5JWHb%Nb"[gM͙m A[:ʰ9GGH ٲ4V/aN%+L5xY< M!L]?UO^f9ŶJY>5I{F1|a$y(XEȈ޵1륺*U5㏞~4Ҭ^Dݺ!Α2PljnzOcE*D릕h! eMtr kw^0?u DCєu*KҌӧyN::k"[Q|)kxء!;mY}K`Y>rMپ-btZ%d )=o=zs}@rRB4ũq5[vj]A+ Y~!oY+*Wgv*U1y[o^-[vT:dp%(rhxkITdiґn=W }oi*}SC]kJ]^Qy|\_rS($}ᗁMiPi=tvۮáؗv~GN&@PM)\|rbcc/^rdȠqѹ݉) "bؿz4 W1<\%l؈ߤ 8L(*}ĖqC~Y׌b՛Tn|ZMu W NϏ?]tH}-g*uWǯ_#DhL"J>q2N&f{<1$iWI6AdM@:B_$ʶשC'6j5>cޒA1SǏc3gEpČ3w?`yNCh"mm7}rSfk~*nRu_`ݨY}Lfr4vrΕosab}& mW!z77T~R}LEs@%NVwq:+db GD'+v4L)˗/B۷۴iۥMN*r9Ո^24(W. IDATʏbpq{kX+ nLbpbQaxF^Զ*`.37k'YTb4[le 5˔18s٘aR`bkeǸr. ^Ux#]Oe9g|`–= ̲*|OSzͧ*L9NAʿA+BR ¿kvjUerB&43{<5L)O_SO:~RVQ;KU{~QG99V|)}ٸC0ʶlmN.Irc߲;*rv89׺O;z+Q>7\){ =t\U6]O𤋮TA=ŋWsqS:2F2KkTe흶l @+z!dU!gӦM-Z\t zY^|)D49 @Ri0//hrN}E栫 3Iu ӄ/"1)eΠ1%c;]= 4GOUM3KnYퟣ3@淭g vhHP 4?QG9xʬ]֚ ΃ ˱ Y;tڨ.jc6xi0GNdHk,)~dmv //܄8ХwsL{XX!K1hg1hQGIQ-}>+g)_^+GՃƏЏvrSSދ`MR|F~;٬gژ:[f})'W<4^S_FƳZ{McB-O_|5Sح@YpwV^vbkUd4 ·#QџA((B?I |!*!<)u"B(<@1UQλA;ew*;r?`0QڲCeV{y}b\靇#B[zСꯇBnϜlR9n37^&4yUp'4z}(G5pH0T){.qңEf;ko!23.g(B!B%DE9/^rerrr|O̾HϯZ_zFA9 NmϏ? R+N@Riߓp,TʯV_FH )jP; R/; Kp%RHk3;B!T gV///kɝ#""ߩ~tf1dW/ouv:vmՅj[TئiL=MUE &UM" sj [KS|ɥi UoP|W+ѰcF3HPYB˗/B۷s\\wn.fly89eaEW{Ae`ʝ"ҢŌtUQ+#ȩShrK\#8Tr9ygo3 BF)+79Cc3?K~6w]G(m NQ̤g;7?S73)Zd+[DIvW\Dv YGڐ}v}Mv\TT6cQ4]ssw| ĆAtG/Kth0\9]$B'm4|2}07GT=D Ǖ]Psds`ԘH$wwwݻKE̚S7wZå0|ٻ(Hly--Pso4V7vj'%P+om?SFu۪ Fs`[@ 13C"SRR^~aNITf–m+yW\L c/^cR``B}%Wp}(*U 9DJ&Љe6,;QMF#6TXT@ Zj[Fe¬M {ᜐ;̹cy; 瓔֔I=Dqʜ)[/& bEb`? NmϜ9,:Sl!;]ڮzQXZDV"w1AT9D/VZ4Hڸo5XӮ]H6'KFMVըl#aN^̑2rN>fײL]xs9&tT?Q}&gXs7Sr/ɠE9fj߁FP~ۮ Ӏ.}><;@k}5vy)ߩOz5b_qVwMWS$ 9'ʲrX1Qg |u?4\+o"D9I=F|cᛏj؟՗e!"i@IL<줺*f={T݉XW@T+J 2qg Y5]:7}[#:> =3}MoٌcpnN^RTRͪ&,1/V֗FoMv{|&ױ7浵0`+ 0dn:pؖ^k;(niI*e na" @Y@2LAGS~?vX, ^}[AQi'2ԣcQ$4* U4'sq Lq P.u~7op2ӆXJ3{4Ke$F+q L1Z_6e 'Yn9~xTR?˥v=r@ Y`RBug=}T#G|ZR@W}5v:Pq!^RCbtww˻);:p? ٔ} Z6*SFGtcбܴl!W!y4sۊqǯf {mTӽBAáM^ͳ sΝڌx݌w;ʅEޯ^&g: ܼ/ZNGy|yiustnOxgtLp~SqJRl.E$ĢWSksb߯}[8%ό>3=}}eJm0 -ԣoi fvQːELhHjk{\"@E!u N<oO8w6?">I,s⏝:kZm K,2ER 7Zp}>J\F ONv87qڭg>fL)⸤݇ZFA7:fNSd#bE;hqL ]`m3ccr؜TF24 1'+TT[շyZGg7|C7t׳::>RfX``VwxS DG"iz.V$(49jch}*SU /Q :  jykPP9kϠ#|gnM$ GZ+=94DΛ4'xIvO g|L" ]:xпC\XMMkˤbvQ#PddX483m_KY5ZࢧY[]' c:F_e T.nu 3|J98ɲ9W(Xre$Q x;Љ d40+"2d\3Ru$5 r@qnP@.Ԣͽ$82QѬb+,2ɤlN@&2~kXLgVΣG-:LQH =8+KqC^=fv@dhd[jn?ٍjfO#^1@fb+̈m>u8ɼֺ?f,/:$! D2VwM)ifLKUڄ9_p5gƦT 3f={F=e~/R+oT| ÆZv"Q=O1 \p ;9H!F5uVg}g* MMTkAV"z~eXMjxO ;ҭH1aѸCh7?2`E"x#y'|.,(kx޽?t> f'w<|r:*r]NiY[3q7docR= We>|tem__ry=oOUϳؑ`x_5%`v@.]\jFu\|<&&&&F9aaSZ(Rt,H@ScǥWoJ=0v㹯Ƌnݗ%E|LbԼT+3 tvWʡw/W\S;gZ1"&{!lja#Dלfh:)s}Y\9ٷORbcRt,ݖ+#NPcNv;T˽^m+mE &/hQG'n  EV3/Yߑ 9/6J@9,kڅ\-a'ΨXUXp 33ԊҦVG^EP.}9àK޾ѽcNNN:56wj>_ZG75jݭ&!+.>[*3{ p*rY]L>[/"?G_Q48ڲ^]C@%؂5w|e^{p7/ֈr@1)zqGz lQY<~jÃllS5=V KFR:vߞpډrDf:gnFcejFjÃllS5{ .*짴0'UfF?Urt;@ Ǡ^Nؿ]bl1) 5S**`'fhO2@ ~%rIt)Fnav鑰]'/Hd9'kYȀf&TkվTAתҨ{n/oцv^9^5i8{\[ޘ֢@ֈocOraGz F2MRqxkuʩX%Q]X!QӐKzm@k??AMdg =Ko2m2z{>-tc U:p>Kfp`pekS?o@  {3RNߺ{EcK߲σbΓD=O˩1~kmHOƼvh0ږ| {ͫp@P\LBbՑJr%.֡qov_≠E/-^κ^\| dl~ZӋ@ Y~]#;/{MWAz9k׮ &#J_Ǎ.)٘JU\KҙCm]]z*O` v*P ̈ $k}WA`y_RdU\]sȕʽح; ȕp1Ai(NCxN } 5x@T$Dz3H)Cٙ8ѬYY霼o:O&/A)!lLS*r3U IDATrr 3gNlF1AX.Lc7 Qfzjwl Y!@6&M IƍBd GKQG%2_gn@ ;фryoTqq oN0bkCe -.¸ļQSj@jfO#^P.L`Naё&2zk3 Uq._͛75Ma-+šW [kܸ-D=&v\—|=U )3Hۉ*SS Ȣ v5$;0V|~QCÐ>.GPc 8ue\-!-QR费6]9lN ZݫtiBe0qdɷ1;pCQqR=vo~db?D h|PM/'XkS[h SgM70%X,b4s/|4x.BӕhuJ,$` Ě(aDnm "[40dS |dlëߑ俐>!(^!=_*K5 N;YUȯ<*zLn4[ǝ 6?i[qm!ȿt Q]h3~Po/aҮ%-@*y*gezަ1,%0-R;NtU-ݾ]m: gM73c޻woŵF1" _;aWLk.PDxU*gʈBʏb*HvB<uX5 O[$h] GD g,W6H6Wx@8bKR5P#uiVXx?K$m!O$ڊvсY8ͦ[>N@]xsn{s)Mƅz&lUO\ƿ"js iWlxs;mq3Iǃ pJ;Wg$Z}z%:ݔbX 'I8w%ːyg՜ Cda]1]E5&/r@ _ l(1՜4lXq7MYvpW áJU-{]RΩ'rmxxy{ 3nAXT s g?wcgg3v[=_*f?P]+ҦJ|}z\#Oy]2Qp6 JGڠO,8ڜX(U;8cQ]Wļz{t8^!V࠽uD/WN>wϟUq~e\ V=mOxr;m$-ӫ͋UM>wxꯉ8s~Eb+* husV)D578TT9j˥4#޻OWKWPuurvmJt @gM# ðVZ5mT՜x5#mSWkwv+(>;3ۥ}ᓎ;۔.*Ŕ )̹s$`e_v@jGĖUÚ2>qwy<!w Zwd,;DaL_"i7i++cle,U'7*y{xNa{_<ײ%l;w,ةݿx/_>ϴ.' ]Xdړz젺VQ?؀C1qZ9%S$[Suiw%.j6W+Ll$!vyk@frm,/\&,2TZGg7|CmvS-5O&-pfw_Aѧۗ۵)vh*>kڨ4+#{7o0cà 1;ۏX'S|;rODӧ7M$1LYz\nz$*$1#`^&\ԃD#|Qf}k̛LIȢd&q{tVuf3ft2a#w_|7ivO-l2]8=9Y6"5?q7ɍB'B۩K-/|г&QAuM\o7 HJ5\[sNL?ZOOW:%m(]:xпC\"afmڬ.'%l[Kp6GLY2Uj*0F3:4ƐI/yQE=++zIq v-I Dʴ5>|qfp<3Wn5͋6t6 t_t"hיֶ޼Tb5 of6Tȥأ͏ZEUXzvm/;̷xae~4bP/9  3`VDTWcf$/'c r9HejIr3 遡gY=.sȫ &RcBM1ٱF1~ ~Ym/q{(3 #Le$װάG2[ujՁFkk@dhd[m(PچrwPY#dRG&}Y!ʃlؓXfLKUڄ9_pQ-?Լ4fF ul -Dd}IK}=bVDR}b:$nwoho5v`\ݵCs1CC1qdAT^}X=ƞғ랕x dej~ ݻwg6ngP"@L~r9׍oysY @FQgxPFu\|<&&&&&FI-|X 8to6䜏{˿QٸV۔V-mQA &>)tn08hira0.*`v@.]\jŒ(?l1H8W?aw V</m7Uu䢂g1>E}\w EkhPޮE6ymY&WafffknS=lվð5Ca3k-EjLťAWf@ɋH[ilm]{.]+<֪}9\Xdܔ5 7(NCKnߌ iro(Gƶ/-§v|^k2G^xFyjes??o^}vvl;!KiMК$]l,^-Pv =aQe VZ*)hOBY.7ifs-:mekכg,)|n nU:*|:/g Uj10tSMސV9{o =\[2d톭?4zK-O i󈏝Zɵ}jNiTU:/ޮM h{;J}֖WU.77Jbh/y !Qdr1NQƓ8:N ϭv(=`jW:Tk;5v/h`_?ԣ;sqJZ;}Hg6%>{ˆνaAZ&ߩG8&_3w,9Mty_&8!}pԨ׈yj^A= O4r#M۬M;W@X?Ř4o.U|=(M㗿1 euawi_5K8kBك|+"r 'C 9ʞ@^5mWј`o.-:=8סJ~EjalA|q.C -eFhuɫK~#N>)$wM~|x9YYYNNNAAAsi߾=(F1!X@µ]8z??M()ww NY32%;މp#U 0Հ6X_ ڔJ࠘t4*:9Մ  85>ͼ<()l r$F!2#'b_eF]B *y:scLU?jWј`:r>x<A(rg#gQm\I^ )ȹ'NZdf'xoZ啽/ي?޼yS5Sz8LxC|j9r#ݡ+YfO!N%\N HXBd">Ӡ`pKq @]L>[/"bv\—}z:(j[reW>_1M~@ Ã6ɗ]d2K b 2kbO8})Дq8;9HK֖!ՙ6%זu f> ɲ][L)rΗSCP(6)#R Ӣ Tdϊ5p@>՞0IlZVYnxGli8feϫT_$)111%%ױyL,OM<\9hiCK9iv+lva]Ή)ܳtFAl "%D6K-%UY76Wj;Sԡ;2oU)'xwV%*_@]]ZxR45@ cu96g[MQL:@.1J)ȴK;bS]urֿ+S>eSy5U'ЍL}`MкC=kOSF3lXwY.P,ْ DO?k$xNJ-U)PZ3VFg0luURT7ؑ$%w:2E&7OHdUFoMn~X:a*TvМnA"ߟdDМniB JG>srdn`W;]P(NPjՕ=ou_wvsGd91v@5e&*e@ $-^_-h3q7tځ)\:oדn+G\_ 5]E5rrrRKcǎgϞߩƘlz_"ӗd"IbB񲄃 O 44/YGuK9#+><v*Ӕi?zrG2Ĩgc)31l Y51#B ` Pz(}bQ)S͉8"Nߺ=2\՘_X@b˗/+\͛7uw&Ԗ< _(oԅpID*nCE#<@Ѱ-6{!aB OlKϋiG(Mr3{$ 8P&H%_ERc-uU>j<ϩܰX Q߈Jd^μZԱdc⛏6-sߑk4+mɷ bܺ<C9@ ez$*}ezS1{V\T_L,+1Or\q ?G/)( _ p@ 5~BOlE$ D]1;'!UzTC"SRR^~Usi]r0K-%p b_q519 @  3+ѣGjfO#^PmL`KЖLOUðVZ5mT<%UT@Uʮ?LT&0nqBjy=VƲX8Ú2>qwyԧ NP.S)0Bϑσq|!~-;fK wlA&޷bb{^*ܻ&{h_wrI)_umx2ήx^!ލR(I4ZLAPa>#tu_-v;;;OElT-SW[aN#8LmcLa-]y3<@(*P!h?=;pyO>*_)htQEzxy{ 7kI=W:*́%Rjwݼ!jջ:h[ZFL8Rr>K.Q^G!&cw2T^O%k+ 3;i3aq~]5ܸ}d39='{JIZwO:XƼ:ޝv"0Uz6JUt'f1zm ʱX54 "iC-D}Igԩ[^ :Y/!ρmfpM;Lڵt8}jX.DנtzֈocOraGz 56{n<'Lu٠rC9{WhRY rCAgl^RreȢ{_d&4\$55ba_N sRlh|.㩿^^gR#+=bB~Oqjھr)[}"_ K7 JV>?'N [M-ko Qso 8w`ř[2E20'wXS^z[p,)cs*üL9s4ȉSl|LɃ7E['.ģzBnh-٦QCk˥\- [011{WElNW?os}ټNLl$!vyk<w(8k ?E~7\D@g.֡qov_(nt2 pE ^gLrHihME}M`;`wrrr#.̿:oZeڄ5& y?#F]6vnE#Z4a_B[Q!7\]Wh;I@ZsTg)5n,a`aN$j$7 lEVf=v!-Xl:2AԊǶBH%_gvFxŤdqjZw|G&Hm=\GGy=g@y2,mS`40(T3oLlh͗zTbx jLťVhJC`8)GxW8U\`v.=q97?>r"W9*ÄPlN秊v|܏]-jY=& Zh,KTT0'U;F@g{EREcV*nY9-Cƭp|~E\hJQ5S5PK_3|_ٸmPu4zm;j1(O X4!~*,YjjjEi3?vrrR 0I-_ =0WwQl(xϤ*Z@ Dm/h,@ jbi}Ĕ%]YM'%MF&y Qm@B ڀ6ɗ 4@ Dm/A @2jl[ԲR8~8f3hЌ@  r@L@ fNx@'< 4@ fX!*;;.@ h :c@ DX!hfeaR HJ`XluҲG/(92ty Q0jl԰m^3@ /'7176uw0!=92|lJÀ!Uc'`z}|tjZU@ \{9vI;:T.~o>}G+%bXNu'[Z4n[m]'ʾ[En,C@ u/rN?\",,^wc)beŧ{`aGY|FknK*&?rŋo3K-oFvxBFu/?_"ag%ݽ%7WL4jdiwu +Pd|>_|j^>\z͞URTߩ%nέhl>K|H}@ Qr.]dm6=w @&4=wkWRHϜ}MIʕ<9WU\@>l?"7(= lF 0ƅWV]:4`S0qx|:ge?2ߤzVONZl c譃-{FnLvYN73#i>i+Kλmý?4dD͠!0(^NJղx\*6B#-4߰וBz@8a~-I]n67r DM| f]6}x?]O6W>gbBQcb β*5pͻE&JdrNƷ1"u' Oz1p ޸]h]f9M#qQS:l&3h@ #ɍJ =!C.֤}tR.K:W"S&f@cҦYѸFboHyr\۩Qj9$z7?>jk{GZb敶7f^5NfMՒc}I{7\*QnX.F.=h]f9O8Q%#6^?z5ңi-Q9dҌow~r`jέUw?G4(t3S S.Ώxzj6A Q ۞H3~Ӧ-9hq|yATN'Ij夊43}3, Z̼7-C6yr-3ֵαn}șr#ZwisYji{\G9kS w̃r2, XRZ΃R9F-=溚@Ӥ6nzy]9Ɩ"&: jEI-PܝB K sٶѩ[riŲmSo~?Z'zzE2bⲳ'԰SYtZ\­鳸YZM>O\7]Ϗ=Lڦ-|\F-- P~:Xޘrm\ڛq&'8wB9f΢5fy had V /@T&U]jsAꂶ6Y6flԥܞm66!sIϰe*/y?_c"B7;" ñWKQC.b_2&Hd_˵qRשv6`M@y9p-ָs[Jƶ9<黕=3>bbF{c Ͳ|W(`MlBcX=Y<׳`ؔ ] 2|߰P4vw6/?'qx7o6֤,orK&E-QR[Q4>QHM[I@ Jƛۭ kE @"b&3[ճ\ԭwpn,hk+ORS[coB?jD`MԢԯwHn"~疧4Y0#q x7d)<6'[6ܙ^zV}@p0 b1҄xͫ lbZ>guRO :y9 HFg}J,YtKd<pE(r C¥_\z;at *.ۢ$'dG; U.^9z*YQ-Sl4c}]].IeIZt5~np7?kL՘9žWN>`bltH>)zVPG!Ԡ[I@ Mr$?E?Ccո>Hΰx!T {:F}W[ "ڰOB-~} ,5*p‰ȓ>bA7 ,O2 e\Miɝ2MR:=A^0(t3/-!h Ôc X"1A1* #\GK>@XظPbyC+_ܶV柳%.leo FL={A*17]PX$sqKVqj  OXJὕfk&& JU!*9>]x?Irsv3__~^L O ُ}zaXn[#Eo$ r7ߥwW\jd&Է T@0 wwϻ2(r_ :'56o>峃^|:y* 4nA)ݱڰaCDDL^bEHHd8>MA ;qvao/*"5ɶf:&h(Wmʧ:% `S߼=s4ߑ{qٓ-T `eW^G)G#w̺P?6w G ͹7|%-aŤ9cE=%,Qוqk%!Q24ã%0uQW0Zvd}-x%,M;NUx9GgAAU)'A}*a觴,ª@Aԧu:x4yX?f{Z+ hwBrAƂ  HTt1vW=[_z^*3(ʢM6 Gޱ ~BJfڢvXJGwAڧR{Ҙ ]+ںC@AG999Zk*Ca !@i u'+Gg &$Oݸ'%0(>Qa4B?Nt-X2¶h8IG]ӻg/%]`0mebGVeQod&c#(:i?g\_˦ HemF=.h7aR\@3#ڗ rc:,kFsmga-wN5jg*lLIII,RVN=*F@o\RڲԍfB*'{d%82՛cp IDAT8:)n_4݆+PiF= .#ž-{T݊%/4 ˷]uY)U75(Ղ1jQ5;cJbtbl s(\ &)tRO> @(- O7 5J.Qo%4RSV028݁ˉw\X}5b>g42dALqrr:uꔇsM ,'lNA,*~"ւ_rfڤXf&1 :cjaf%$\%d^c*mtK*x~X;t89T6;  Hmg^^^IIInnn%&izѬФ ^_0UYrG]NTsO~v/&ѭSw#z?`]vbn|a, `S5tmw9 bѧ#O=0 )݂vԃ7h '}ȦQBq4{0aLod[' jӡwrSQk{iYO7cu;g5ਖVm)j@CirkBVՊ({DkM5 5Cϗan]Q!1ȍ rf#'sUkb0\{/7g=]сUgªW*S忩œvV0 >1^3^>O=MәaTZ`Hiq_Z.u[m?dWBo;̋MgծKѫӬ_F_>'E(nf;yM0g$0~^e;oCd88F[j?c7a,ˎdz# pox;Z,p@G&\iն>ޠîuLsJ1H,*22 Zfq{?qIdأw2ԫ=p7%;񲦡ZP+hZLb^4>}NwJ(CJ+i{{)x}/:捥;N[X;*">EP|3H;vFNǧ >>MV0m63&m+mIb. -gB9YdfYxZgq#|dcN) >U5qkmVOzǶ{79Mi.2 [_pbãmF鈗5#yT &jI__s2HP$GZ XqN4?ݠ`||6/<޷]'KܹcAP_q"\w$ ^v ",5@A]t4HrnNΥ[f$eϼ2,pgMt]LEPxl>)kW *'^>|^qdH8rOߒXܧG Hy;|;fMTwCfY#c@sX+fC<cNVw>,>,ʺf^𠞝E%_:q PݖHzgpD2Y@}S %os|~d*_M 5Br"e;94_Z+uE6k.XG_7aj֞n7 Qr@@@-[^R_ڶl,*nGzy~6.Yvz+ ߌ_$w^dd Xٕ{vQ-HlEg-` ZʋW)Զ`MߵXPZz۾eOhaCfctL1{`֝4 qIҘI.ʀ%7k1WP̦JRr9X.]L q'lљ e4Gl GJi)>lWIP::kp5K υDG3 ę8NjB7.'qaQ}9#xĖZˤ[Ro.ѝn?>RRY49P'cbdVGf|L[C)G/ehO0R`J5$R=Z˼YP̩*Aj+"heQ٩?׸xNFLrQlGrp%;kC{}hTP̦ԩSΝי ii*8\ŰOj-W ,X9ݒJ ?.a2Æ:rzemj,'lNA, f(0ooAe:oN)sZcΗn~rbbeӟrNPlCOw s3dGl׳s/u1JA V 촲 ]?NewlL6H Pm$UݵJJJJLLtssӗUlZ+OOG3wЌ:DYu.?݊zh:uwO1#ȪXIv-mGڏإ蘺T7x|U޹!{ ٭حs\{ |Suj/bحwrIqO \{yĶO\l]Z7dY^fۂ Hzjf"Yͅ@˧9p%)[޽ fz\gu'2'INO<4u7|PMCyso ;+T 9~IPj~OeCSF΃YlE̫;Fϋwi_fAX3#f=뫬At󮋷i 2c*a&)ELBJI p{Whzzlp##8jKg+R"bIf4j1T)P29:ݔ1wU?G4V :-j,jUߞNZ"q0:N9d^%Ȑ4,a^Χ**@U2UJj99Ȁu>tZՌYHժVB(o3Tȿ3p%Ov=2'Xyjot!9R*K~(m\Z[ȪkPά#8eP7(b *-?}ت'U*g^֒VA0᫥w >$CA;Th HII1ʱt\Iut:o\2 aW2dyg|p?Eο@ZOHֲ Wgޖl@RHPo[PzOM2VA2h֏AA4WWW**buEg2s?icl Χ(#9 P`Π_rv?{#9*BLB'5)TYM[XadikA ;jH[ՂaZCwDNi899:ujΝd2y…:r"- _<3 Tpi6y;VP ]0ik ߎdx ,'lŘ-T @)Ȍb0 DY2yXѲgUmRSQfp*PQƌ*]uSBکP-Z#`1V^^^IIInnnҰӖM]+pv[|掺!3I~ksט(ьQrȅ 1,@dCd2ApI\#N8{؝qk2BTgx{A-6Eƻz*퓡U5qѫBUJ|r⠈ ½M Q-hD\%*xXD'qb`ۛY3 ѬzU™_ʹO&Gldl& i! H o4s]o:/>ڜэٚ5Ҽ/E T/ zM)kRf?`7f^i]mS=d+4dA2WLЭIJ@΀boӢ÷)J.|h*TJEg2b իCa t4nqJ)g>Qٙ߯//`AT3՞;=F: <"t$hne}! uZҚ4 IDATtCO? Z%U&}aÆ2bŊÉ%JI\*eDT]y/ɶf:&h(^dO3tD'q4 39V$hp\#XA4 ʟ4ղKpA1#[_sאY$hySXmLцE9?Ln@#C-eV͛7,YZ ߿g@D^X20 UJ#f~&AnL]e-hNr@_ˣ 6ڍ'`o$Ь}1/AAJa}'^4`񕄦#Ίh,har82̑I!d@fRu[ǤLk)))e |w`4(Xr[s kcn{7a-B! .#ōo\KNF蔛ϻkqD>`B(ϐ!?Gљg\5qֿMb̨a>~ރf !=_:g؏s#N1FWWGo=wϕq骕:Qd>iwn{+5^T}Z˿A$YRLrdlllllۼ`ˡF4q\5<6mz_?\94v؀>~#c1 38[Utw/PJ FTN٥EjX,3{ :f{Z y4G@U&Hq̃?b|F},$1!>ׄzykˡ|WnUb~K3;jQ'Y0Z$ܿn`}Ծ_Cꌚ;>ʀ'R:-M^W kf|5t汧S.^xe8?A!$~œ _4ЪҦ/iZ9NNNN^sV( P /?L:tO~c|DI->W? +3+B2ΆRplH}RsgP?8_tTǮS*69MAcwתdհ"G9bgB&O:lκC #'w)>+Lq"_{NuZY Q5Qd՟4%ߪ2Cٹxŕ6};R=/XׇYcۛy0]ma \'8{g eaf+H &aOأ^%W.ª?m;Gm9~~ ,u{TTz}!J釉ajߝ>[!6Z?V?MSdҬ@(ƺktrKGugi#EN;G_ݤMaoHl ߂E>H`IsfB82[F_kC[º RM&* dY @(tTNNذŧFO_>\UN0 ym_丙$ Z3keJ)>SqԥFaolʮM}mS, ǡRz=ClLQ?Յex52 e 2kT)QlB'<>f/vLke$}W%Ƌ U/пljšaͤ\ff׌rݸ޻έr7N[NGqKN[3wP} y˴eT)>ᴥFmTDznp=jJe0(8 _T*sgyz1E\)14si3f(D)6lf㶬a`[}h0y=2i=!W1gA ߰pO-d%Tg%(,mN#Ǣ+Ts8ÅV[:aÆm`G<;ܖ:Ǟ ^ ظж笓i\{t|_;Wϊ1YC|0d\vOQ y4O2r~6lfsC 5|=P V$S'߼S֣+z_W.xŕ6J>暬guZZr+4 ;N3 ߔMΝ uXFjTDp綾+q_o^tǙ%qSHz{ŇV:}v$dLq~. e1|!r.i/BTFY deJкք.clwg<:PXl#8|'"WqMfPubGc[5>u,vmxYGO8V#2e8l<01!>9ϱ~+njHMs'cFKmX!U/ @kMe1jD^#w&sD>њ:rm Qhրf 8Ql?$$ǡo<\0$~JXwY5$>ȐڧʀZ9HՋ-M} AFqT  5oꗃ  5o  T5:@AlƟ(TN;Wۖr\+xdFG  j  R;V  zP   wBLC@At ޱBAjݱBAvBAAj'AAvBAAj'AAvnDϣ2nY-f{"Tİ\ m9N +1,}vJ}ęʍam,ٹTޑ72H|nBDDY}.Td+CAAО/'(S&C%'-:$c6&YXk%r\n >qhǿ$ԹHL ޿T9.ǭʖgav˧6B4w84I v+"'bF2~#\ipH4~M>E qHr]w a2\H02͚Py=3qeo8:*(=̯?ϒKer \7RCܱ)Jm؆q/n%SӺԣs-ڞ7m fE^ͷRܖ]f  }<$ہ{Gq<Ὓ?MXV8] Z1[SE>Ȝ\Hɑ8Nڋc\?4tޟ'c_>GS.SosI۷u_nὑ(zΡ>9P^9bLZ.Nߓ6G]̻2?=  Bsf)(Z1ۅف% ۫OLgeZ,f_]dգ:O_bc2Ԓ(|C"aֈǝ^|lt#s@Ax[0X^IPgHخMJDZ,fKߍiL'mN~Tkރʏ> K3>k3^7$*i}7SҺkm/f2(PB ~r_HMX  /'}G>[pU q|AoIJU%\1yXBT1.2|ɦ lŨW= ( b0oU$˖-L/\_xA1!w[/v*/LhG}bh!f]F_KG-W`6$]BokN &^ŒLPj]ޫ"s_LKV{ AAnJbR1ݞvq%͖v"}ĺ@= .5#w=I_?exN-Zc^{R]q HAA?$vp"q A5NY~};y3I5ߏjX5 rm2_|~[xU;΍,4ˤgd5W. :V U̢:oK/?н{#9rʜ>m(Q-;^k .wYuqh߻@I}ܪ-쨚[3v(R.r{_=#ڝכnfu nԂa\qj]2ZEEJc<7.zO@$-]]3V7O1꬚2%)Z2Y 50V4deweb{?6`+ǿNԨ}넩.?`mص"Uk|*%ޭ 202l]@Vawyϕx|g&moQ)UzMڭlMO/VC֕$iZ^bXowDSujث"#F G)bvG[w# wޙn,o6[UFJٟA-* !L ]}/"R1t[b=]B1< 2>׬Vr[>[ko_QA* R܆Ŝ?k[i&@JPМ ]zTڶ+t -6!c?;~L/jHNOM8o>bcf27q~FYz!ӼUnq)i`߹DoXS *D-d7v'z@rJޞfo%۲[)rB"+1YuDoʐ܏4@&ֈ:O;Wv FA4[hY|S\bvo>Ed$YFЍ rKӀ i"P ]jd$lrRDƔ j RŜhݥ]fTLԬ~`HZ۷}ع1Ky|U0/KILS5ը]餴'3.:**ik8N ' lJEz0r 2eLR};VT12E딄[Cr3r8iHBI|쩙>͔s}\q̿z1Ŗ%W6Y7hb8aw8|ʇ L'}{+$.AoӾ20Zr[2dֶ)W T2Aٮbd'!Eo>|Е3NipH^,,Ѯ[$L4q/ j.x9rRΥrf->xy]{7ɇq͌8rܲ ao.=LerCW%'5aOr^$/~2b>2{ok71.Jƾ?XgӺW#WmV=YI%Ժ)~yVot'"i IDAT]uHU [ .5\W.|#G==]]^j;\s}+vTx.!dֶ?ӻvPUW^{l)o YJU\. ȔNmU=7/kF+VX!j1jq~NB!AX ExB&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!l*!B*Аv !rPKKKv5/CCCij^B!ls9v?$w8h|/ P@ҎI_aaaS W344u9|x !B B&rp˓LQno| L&Ӵ)L.J7B1n2LCCCCCC69e[O]GQWok3Չld;9?aad2LHs"ZY˦ŲYN?3 $5T ҦyOvE<>imX,V[Ŷ,˄rW˩YX,g&4^[YUe*h$i#]'qv~EU?2rӢдY,kLiJdM(BE*IYN&fٯ P^ĭ˱nm-@j-y|v,ѨvOPh:{r_gw~q[TV~e+u&DP="8`tb+f^|^%2AhE,y=qc[PڞݦU^4D)~4DP U>K"\z%z>7~Bɑ 8;cb}7V;!d\\#vĔl [wo<7'@(/J:43˕9&um6W~ܻkJW3^{5M@15%|}q|+/nqz/(8o|Z6Y|>ݴi9!#g9Jtާ5oEhE\Py[nk;kGqzpQA =f!$]x $ʊa& fYN:I Z=yh;GfhϺj@[ceᬾC64-$6msY߈p)F3MU hs9lz^467Wv7nW/y:~BtC;9@e}d7j!dV9E2L/HYqk~i9`mXLgD;>tJdYg-O8w߇dNҮ];-%}Mt SCWBI"ě@)twA枡ɍ<3g)onP.>uH%ϏK@W'N4.տQGx ]x@Bj;M9^@=} RVoo־DKj>,m$O>9.S>[эgR [^OBzLş&^ЦJ*W |V߉]WQju\jAG6Qo@4-?okf@<<;4wgRSXnVzs!GH`2mP{ =J( FDbjm0YpP)OYt"*BGJ>^} %O&ꢂBաlܸQ1 髬lo߾l2Izr˓f-KHwЉ6(&|t@ųnfw7s3;%Oo|! >We\x$q%[o}e?Nx=Zwon=vwSUwNWh}_Uts5P٫#{f͘yj78}{g:6Ob̩;):͘flqD a9W"S!Wo:=`.z3q-W/&:;N'^M0@h،:r3pm/<|pԩDVfe QG_e#; 4Q/TY/VeH޽{===Bu,wT. *yE],2^ 5~['B^B)x}Ѧϟ—!#AGA{H7ۆ+CijFAюV6Rh B7x + X!*\ K;;ZQXi[inBUs9 Hx.VW#$DBBCe77r?aB!لUB!dV9E( 5?Wdn144qpd~Q ޞopWeh͌{œ/'BH8<>H ӴW1 =OӮ_k<̻ﭽr$aTg_ i?֥[5V)ku0{W?c`GDW M2:B6N0EuHتDڹǔ_8"ZwL6w3|2g~ dT2U:424+Xxs.?=)PM[}t^[ս;||5HL&RW>oyrvR /?J~ŷ}]cgP2ΟCW^;@C;:ޢ堟"ΨnZv$%啕gX80e[ ;9Vq=wqG{BW38B->}%D5yOhV$+wd$ #)i`Pj@OyqV?eZa3;O?}D_^Ǵ.NdqgVȑw55r=ɓ KwT|n)8{NBY+:ʡ?LiCͨv:q9Z/.8PKUh>ewvXǥgV~e2F.v$]%sO9-ZVGّ^K 4 䝯ygy97֚xOi0~eNoXxUNGv lC_W.=dy:T dCNI.+BhwW.~ݷ3C <Χ,g'} jjFf 1{?ee<E#2UةNr(69$q)h$Ю_P$ X"?堟-.f wamvsvnAdOR R/p>s-_ 4htA)?]vg}Pԛm}5Kg- <#vKD%QBӬ.>U{Xg T$Go yaw X줪hZvh5yȽ~#$\g%zd؅dJ[-z8SDXOMQDM [tk_&i$*+/ 5ƪQl[C.[Ep&%=f[*bCso".5(TSjm\>5*C 7zR }g6=<*4,'6r^71CfKSKzj^ۑ{>XWB?|IOO8c &m#[oWZ>>Jܹ%NciK0ibjV= Hsl8s?l؉?.}+<|+/" @;%r'ӴϯrvB$;PE_"NԈ9AʹtJ ::42cB(c7H Pdn<|!a6PL֨=Vj-qIj9\5gZsI<0_}gU)T.C7,nyDoXg=%PUSt&Pwm[v$ڗFrxzL.E:lq}EeК>c em-=9>tFŋ63wnO.(-"-ڰm_nrߐTDU`wtw\ >-kG܌{ڨWge=7Pgw'b;Ǩ/wO/g~#3X“+/|7tڍ#%v1X:u&5Odn1 ٷ· Jo\?򜇻gSWRPcDu{?Iٷʋ>ڹlOY!A?]|UoEyֱ. a3<@#3G_\;' ^bMV}<^[8%$'wݛöÃ[*S=k.;Î)P@ehk_,z44'87FPHW/ߧ`a_r&Wy'YYg|n=Yߜ7rtsК=a\gZk!$b~6*ǒyE(qڔ[ǯ,;Qp`8TgC M{3iӡ>2M{ 󔨽{ukњyW$K!Ǻ4v#f+5& x5j˼Xz6d܃v5">?`]hS~'rx !ԼyKQR c|Wto77:k.:9_?M%S|C#S~'P)w{"5G{$t$/r'cV8v- K2տh6;؆J.)O#Y '*kAAet(7N)圥cڕ惫пEL}2i: ؚsz2oNA*?g2.4p18u\Ql[ @a=\'KRNYb\Am?kR@ϒ!zؚ:aA:.lpn}i_n !>PЎވ|fTug҈H։p7 {xJFϒGX,v-FFn8FǞ8#~= eEmfqx5OZ{ϕq&wgX-q/ٯi*L+26ly˖4%ӫ+$.[QĶt~:o(ύ;ګtϻq"//cߚ y_|375V9;1V-$v>yGB- V9Q:)1+ &JLL\kR;$yJfZ vt! ]Vʤ}ߟ n8T Ϋ̬>A f5P5x9O}^2Yt6-*޽M6"9Ynyf7OEW]Jf+ȍKxEs٥ F jl\ -gω4P:K}{e_[;xe5iii!P;{mxql%!S]}h?LiCͨzv:q9Z/.0rPUjF4~n2ͻj;|ҳO*ܲ_jhy.M֒9L&d.ZVGK8~kR)@"QDQ3(|=eWll oKekR%Fe@&Kj D@ IDATKxDUfQ۰fK$S^mZvh5yȽ~ظI+rg|Yl.A!va.ҖFaNh>HU^)TpWW`wٝ ꭹFAcu}ͶU9^,7x.]eG MSNȍRr^UrvRU}Ъm UE$*s*B AͨcШQ"?Pz'uR7~g3+#K+yrj#uS ^f91jFKy 2M J/g箐t쎨OԌ9پ(4ggo)}0miSG$9fw6Yi6[o}|}%QE$x$^u\h>K \(ȄU]S3hozx_'oIsK!x=''4ҿis ~M H0ibjV.qfʄo@&R;.B?AH^z0G˝ˊczfYBUIKEg'ͩ}ӥZӻ^|4UN0V4Ǿ whqiG@H\BHrS?X< Is煇{'숐|uL7kh[ v#j+s}n5# k!$MUqȨƤ_Ȑfdy$r#NxhwlC<>Om~) c@KOOO^x׼ut+ۙ@骯`Lk=TK*[Wk|~/m1WKKe5=(w8Cעm]I/.dQ];le<\Y強>9%|ZWΧ( M6o&0ESUQw!Fal`UJGQ;bp(KoU 4*,, dAXjV%N-!h"I֫{NID_Ivr$um{詾Y7aWM L*^;լrЬv=J8,+}ŸnmNǒOLWMIޟebWu}񋊇.h-^A)+-?w7}޾[^)⎢1AJ8fORXүwR!)*!$M+:]Ľi%T9t($gr}J n. $:;^!U@AIwkc_euץo'wN~rXGtU*4b|"pbQ}`Hrj} ԧ0Z,xU;nQ32PMYlU %z̫ttG:L@gGV;C Uγ=ࣤvt# %m(fd6 x|O3k:huȨuIV%E\qjZ,_2My~)R>ˆ&~[:L1Ѹ?#IF=gRWϵSO6d; AI]B.q0A'Z3 Ps*!$MFr4ǀJ*̭BU ~߸3H#t?\ITR銿7})h+k<ڳM/] >~\^`SϪjGU}eoc2}]S/Q4*HIFa?ig#РTyּQ@g*!$ez]:p8r;ڣ"v%*oYиDUX+]ktc}rc'ǧH];ٸӎےmV=YI%|OC^d|rOه*Kda5Z_hO?{r>s4^4&4Lw)/%Ť־uQV R jgP\*Egf !Z&+ A!l*!B B&\}A!>FX Ex !B B&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!lJ;$k444Bg(((v5ҤByJ;WB!$\j.4o41 /0Z@ v H j( b344u9+V!MX BH6ad}-\1_h=o6& }f?>I)7ˌSs:`M?Zr$M^aW ]'Xq΂As#2C\1>_GуK.Zr#QV3ax<7;ϢxuLJF92CӾf~T+gUŷi1 )(ُ?!ހ {\Q@ }tGuSU(#d#1[1{;b9ƭv2g/}@wT_P}EkQ&4 jZՈR>2~2?/jed&BTJ׌ԑv'5X/qh[Vjg$2! 4&Zq  ڵ#[XTpI2>t5:($$!y[YyO"S=tkX2?|CgsaNDq)DfԉƜ`)bMhzު+D x3#'/a(X ]x@Bj;M^}ߋp~dОz{'ZRӚC?'@AszuQWWUrnYN쏌+HKF|%#7J;$}M5ۗ-[TgdY2ݿ7t"$ʠ}/;]$P}lg.yFx[g#|O8s^T3B$/[gnZ$|cfw,&3C޽{===UPoDmNq;Q".`~/tp[iZN*?:B-D6]'~h/PV !Z2\}ov WԌ^mW P W@\2\TcFIFJP XrBUjp]B!dV9ɔi`$B V9!MX BH6a- ZNp6~Y%< Bt(Ja?uajقEzmBs>4Q Jeot6r܆Odn144qpd~Q,j7@Z .4kM8=,[gQAuUYЫ_h iLtuDuP38knl>%[-C|g.Ln햪ثr}lc&W( 0evC,z44XMo'\Tlo<#qŵ#_}%t.ɬ!Լ8e/L@Zdx1;2V[(4=_V*, zFZ8 .*ۜd0>y_/]YC*5#5#{`gcw~>9 8Д,fn8FǞ8#~q[:|Jj{M`px: 6;^9q9G{Yh Xտh6jv+~c$=.\ ;y*DZj!J4 9F ]gokk6[8~BR2[!oFͦiD%\hl˩Y4T |#>~L%jopj-AҤI,Z'Dh>ewvXǥgۊ,t+I]%s,ZVGWkh=:E*B7'b_xlJQi"u!{I E#2Wc'SU6īʪ'qSQ)jFZ2Q .&kDYRĿ#퍝5Z,j܂bۮpwfg?W)ɏD8K Um;BG+c҉n_ 42<6 Nw񯌟.>]xs޶QEw X줪U6QF]_ UvX5*C 7zR 59p=?yë|N U]S3hojx_' 6jڡn_ѴD$*f14-(+W(.vbX%AbJ QTwL~f 8-%===[+mJ0}RK \6j6EHK&*Ee9پ(4gg@;bAus9pPw5iǫa 8Њs+ fZsI<0FB*=D*gY IDATGwGWu_ڼwЄp2eCiX4Q q0QT<ʾĘ]vLi#,mJiI&~65#⻇ IŊ>uyu:T}4ey!=+% lw@m7x!'XQ'cL(#ECU*!ԌhJ65dq\w'l(4=wg_ i?֥y[=b&PHn.* NB!/=$)7韗>I2DZ+S~'P)w{"5 پW"(5%}zt{U' /1cˋ.{$4*ieO yqWQFӤ~ժTFwm :.¨Nc϶~rj 6OuqSeK)K+ܢyy_y&TT8Z<@HãVsiUbc]e>pt&xE(5WKW^۹cnE ^<*44̥x‹̅k}r@p8Cf_ǏGF] *OZ:u!|O./*.qs#*xU!_4S&0i8]<͊d }U` .<p-fu!$]X ǡf`w^effzȫB#2iug7=;>2뷜@q~wknO7 Q1ϽS)z5/hga4GgU_ogVr蔈ϕ%&&vV*ϽyckR2[!oF%"~#U5zsPkD (t2ίIv8 X!<,>Kd2 ܯ:<_FgWt#nɺ%ԣW秫s9.ZiRɵ-=qqm_24~nڻu?]a ʹTSY:s ?Fd>u4@Ci(Q};8ECHAHR/p>*h)RA#C۱ﶤ{ ӵWtQ840[O‡**r(4t+q< pF> +/fJ#1QD=s\,?yۣ<?^Y1a+Jtg;4Y(hU9 U]OKڏOBR-"2ȩM-01sbՌg[]p_f,zZ?YgÑIN_R1bUSHo'DcǠQFE~(2FO$ڻ۹KIQc~\o1waaB-&$?#?+J,%Df1cc3S½n{VK>ys^>'zu{ jh/u{mVj{@;ݞeG4ֶҍjaӁ="Gw*;ucw~#rY""2~ez_8Ϧ򅫃TjrwNJDS|}7 ܟ&pqMָ +?Rq܎MȦ|RI yvvM Pvv/^ߴo?qh/o[ Us_ёU"17SU>Tj4#"???y ۉaUr3ξ+7Y@8i/ T90jXk.0|u^tW+:Q֩ bӷBV;u,}Z`Ohк?qC|Q=8^UվU]7-us ^>=>SGa&v:BU؅UdR̹㸵U^GU@#V/- _ ȈH%}oЭ~܁N|tiMU/&DqyGW:V]11<_|`}Ӂ}s¢*0h 30233۴'CDkZ9rF& ?z9#fv3^؝{`1(+aXo97aЋߚMTj3zZWi_;$.H~4i=\CDwY-=$1=HQ*V[{""xuW|)e5D3BPѣ>v"Ruw;DDCD<#"RVy1F툨IiL۵g 7 LGD<~$dyqžsVhvEeE\oi+Xjڳ{/˛I!mױFKr@ODhUz*?ɜ*_)6MDs#c$D"OJM9beC*o&4GD׋eݺED&=3–_QZrnD-[^+gZ 1=3/Pruƫ=*Ujj"ŐNbM|j@$ˍtrr*/~rUJ9?{ss٢X r@?#yr"vYNСC}7i5l̛Őo[ǖNbx4!V[*#T&l:пUq{BI,vF'"ҨWwr_t嬃9=SLWLxLoDn;KDQƯLo3|GF-[ wn`b㪭YvDAim +vE?-KPU7H2]B݈H||֭MGnF]"Ø<+~ӵZ#@u3ykRfgz88~3Ƒ+tѦ.f۶ 95K~65bXAW4rwNx9cRvw70j4C7q_Uɮ o|bms ӑs/.|=џKivBEDNKKs\qx`ɻ*Bn7hQOJ URCѼ aCږ~KO%qNZN [֯BKv۞)ЮZTy5IߓZW^t퉱ؠPADeDv9ƺCe_Lh8 8๓g:+T9@Uޫk!<@y{sq߿CD}VU+_Ƽx^40B^ǢJa͖"e?q-^raa{(#ycY7J]3f(/m1{PDD&ys?5ûo}䫉s|&yuF.9Cw!7bnАD˒,[3iiZhk\o<83ΩWV_|XI4rrwwc'aIHLќc⸵Cf[ò 1ޛ6s{y?JvOIoA,Z`&TOÖde5wlHSbv}[Ahxx̼h;[7mAo zĢ#ywg>fŖ_;%l-Nr&"nMOp-!}"2mFD#kGDc|ډԠsfVD[צloy(mv&x'KA ߤ>'t?qG_+ /@`Qp-dJ"eX,d´֭[7oRG0<%oT@[&#u#SMzU4PVgч2.aڿ#oEDu0Q>D"9sfKZ';߲ "Ye hZUʔUL\7T9pSfDT "3h=:'e5De田{V놀 &Y3ʵںvzwHV0/ZAd ءʁ{^,W,?TxQ3 e#<3hJDޓf͚ͭKOfs'606'$s6.Ne]&._||+S,],sT*&{w7+jΩ_,<"=Y;B 7T@ jHOkr@?*/ET}P2C>G.Cff)>F---Sx!@@ U'>x?N_rɸOu*:hB+N͈s].<}cc Xeu(ooYXGm@gY"(}5[kW;J(Ж3Hܟ7gUъ;L=x CwHuTۉog:5DGP*Ei)ؖ[+xz>-OMtƌ >];HФ h4;1Wj { @ʁUJ<ğ]=?<|kg!3=<ǎ[X!ŒD? ;~:Kytp3gBV詳ShX,^s屎 NCzLyW뾉d;//F]|,\>~BF):۶-ztXMFFL(Z1legjT9P g-L bǯo8?!οmu[+Yt ƆnoW=Ϙ{H[^Q],]Qx"ꯂi>"""f"R8ne"|lڌDDl7yߊ rD{;;VPL"zXPDNDJۍ[%'K7 KG[  HYn>mv.!iTOlyJV-涯vn&#"{p γdK~7ex qq=ںUeI2u%{P\ ZZzU"Y?E.&O $?[aD472f`S!ג8R`P/شQ*كѨAi Ԭ\֘2<[A[p7[u/G2ym.^^[&ڃZz#VPMZ:rTw"2:ΨDDUN^WŐo[^G9Mq\"24'gju)h`l[&qWH}x+0{K!qxvWxLID*imY 6^&:?=<<)ڂu|\Xt!"zKF'}60|>FWkk݌vױU{,}Az_qy%,1-& th_;$.H4ZrqH$(b~+̭=u<,=5?gNlacp?0Gw]%qg-)a OPo%dFxXygzzzGcFQx"ꯂi>"""f"Rk'1lݹVIghM@H;G2kD|ӎ Ww/i1o~>R2!+r@ ?5^ýR.=5 *X,3޴R)CJPcԎEvY{nڳ{%Ab9=5,Y7#Y.÷V^[r H/kV򳟚{1NuaD472F"H$ԔC)6U͆6Ɩ-"%xt7k%ddNNNW%Hq^#2c"2zr_fR*G.FC:ϖsf7i)I#yr"vYNСC}7i5l̛Őo[^y4!V[*#Tcw~#rY""2~ez+lvD3.>Ԉam^\(9/]ֆoGDJٕ*#Z8<0]QIyCf4c آێ]Fgga]И_zi:M&ELԯÞGo~9 m; =-ԫLQ7I#Z?NwGMjmмZuNMh2z{y9CL?kL$5ҁcf^^5Tw7Ըz2)5]tiT2i~vD>Ck{< ׯָ#aH:r~1oЯk^*e #P2EZus0U&g荭:飺JMt}z;]TwSNz~h)o):f ًuFlTiwPZm.lѲvkF&qGFZd:7O1ggc۷^-:st-ت^m{QY^6>{2iz&}}ll1g蟶LܗROkΘҫ^Sޤt\/O>cX-o qϵ}fLA:_[g/N&Gz yB:OGخoW!78AwCO숼Lg{[סKZѵOppZ~1S֭Bɦ -j1N;[_S4ePzzvZw7&۲~gsN8r$}aQ۽St^xnΞ;%}}:ww7;u~=T;Ӕd>A֣ƠLSdtkүܮ}ǘaۤQ;ϼ:EL=>oۥ37&oHTѴfS-2u_X1 0xNwgތm;T!k}/A Ϸ6Oבu;˾9NԧIΝ֣3]15Ic3=ڸ;O6I2.[vpOk!33@73o']uk|N1^>F11o(ln k܄~}ۥjZPI(خ1ʵOӓkz$fbӠ-2ތ=.X47yPvlgؽA7 'Bn,I:pROs}q2 yuqZ=N}2<ߤnڮ>S1 3c}~]w1mz{Im~H D#P'LaZ4OޭצȔQl9e/DU[om#kJt4飿yu!;z'5oifSJ]cW{G$]F}lݒ!n[pb~sd]O߮MGopǺpnG[}MobG)4cWv$פ_)&$SZ[S751Ry hvˠxmkwM2*" 04Iruq cAI&4IC;ki{Iwuy{=d1_ǎ-&Coڝ /a1L].ݳ}9T_8c~vFtWAssq>^Ccm2 h~}cؖ^5gE @}qХMl64bdISM}eN5Ijk/nfC:kjVS>5BڡO/t$) [wk̀'o̠Ξګ>-GЅ3zɗ٭&OC*/Cg8C }p}~]ew-tlwڗI =I:>:_Cvmu 4՞?dZlc]}}ad-|UѵQoDf_ _pkIn&ZuTwғ{iNKX'6N{[ ZT>A e I@µ<2,<$ y@-q5G<$ y#HG<$ y#0J&ɍ˜a̙3GI>}:YuuuU\@GTQS N]o%IM6Nau}i\/t_nҽG:ONф?/HaHO.?Y^EqDƷd F$ҹ;'?]:V4f@։-:V=-WԱk9`n葚1q$鶟7l۽wR~rBI҆Wwo?՟8Jmт+SIѣ6CfCK刺bnKߣU?moI-&}C#-sϭZJ://.bˣ_?qnYS.=yϚkkwf{hP yӔڷV-35Yiv{_W?:Okcu$I[oܱzO.?'9YM/ܽQZ'j֝o^gO=Xd]@]e?<9N۷}0O^ }﯏?un+?@fV>? 9snw}7:jd5ҹ O^$M?F-MojOn05%iڄ1?YuLﶫo`Pܥ[wc5m]YmiOqH8E;{?YG0EMƈ}9\[uq4MYkA?xד[?oTsP^=JCS=}wru]%oЗ:mng9Sg4`ڽg@o$;ilq[o@ohn[wQLxM}>{5nL|4>6MR$ܲSg ^җΙ+I:px-l3u'&]Ly˺/h5'+7{cGIc3иǎ]{ÉӬc]bk>T?H3[lxCw^Yv_A z魷50h=CIҴq2MSF{'o@]'cgMԂ[x]{jnj hyon:jMyywhy0tB4}úStTzY7K7Ys'oP+xIu֔q}#HMIW?՟/ܿY W>L]rh*o[2KsS@lL/kee1M8U3םc@WW̙Sr@hTaz_۴woiY }cP_G<$ y#@G<$ yH@G<$#H@j2MStV\N;MZf.\uUunVOy{d&0V^Vx≺;usO?]vwڥe˖5q*i``@]vZ[[G,7MS[v<ֹq +W_8@tRY믗$iɒ%Xl$_+Wӊ+d&MW:.kz릦&uQ,XPo_+q$(!X.]}I''PkkoF7߬-[(j:Skk.R-ZH]vLԪUzjz:C\_+K/UPizKwen_Xӽk|E^x~߆.@ _.M{^z{{vZe2s9ھ}.B}[g> h``@W]uZ}___4b;3mϲ_e2{RX۫ /PGO[w;$Sй3gΈ2s˖-mZJz.R(Wg\3gNq~?Ni*ͪUvϟ/\VQGU\m~ yD _SN|N$͞=[7|.\zJ?-[gyF>N:$Iܹs}L{$iʕE]g5{l]|Ś9sh"IҕW^y7M-[LfW\lٺuN;4-[L7x>OiժUڹs.?c=k;?yM4I>$[Ə?mϲ=>}ΣĪ^Z}{]-VEI^ϻsRq.v;so~S?`D9}]zZt $K??tR]r%uGT~:Kr,YkF7tSk;v2%I7p֬Y>Z/]_a5770 }ݚ;w~lڴI4aM88L3k,^ZZrW_wܡ˗kƍ%IӦMauۆgoYXyfI>f}bUSSn~bnǜT\,\fI=k'NԘ1cFl:1cƸa$Q؁ąBAW]u>;sL=sҗ[nsպug G;wU;3FT6Y_ƦicՕW^XBmmmy睧 .@cǎլY$I۶mEnu Mmx}{zz4qDyXe*vV࠶o)&yq;bP~o3gZJcǎ-.[hQCXu'뗿~autth„ 7o8?Qjc믿駟>O8D[<>:ҹ?zٳoG?>(^|E-YD֭s=ú袋FlƍztgЦM}H?kɒ%zi&}Ժui&Ϻiӊ}HL qmiŊھ};'H!`v] rӦM̷-$űfƍĉC]@)D5H@G/g$uvB#B iN,hHE|Z@G<$ yH@G<$#a2 (s{2*4eƈVpv{o;@|$>u\.eG4h+0:k@|$> A%C@@|$> ZsvY- >Qq5H@G<$#HPaÌml~a@]&ΠgOnNhc)5mTFS=\Z02aHL*c$qIy ZLy/)>cN&- USX]$Fc$(-rQ@=g@50I8H%SFJʨ1Tu2@| #e<IT?s ? k^ˬsxՎgAʹ/ݶCAd+=ljpA8J]Im|Hf\ۭ:#)7(Pq-ʺǨJW;@ƸV5[7*G e5t7n-^qGoj*T P8xo[X񚍏j$yRx<=;q EicQbט0T>ЙV+Z>%PW5']@@Mkͨ*K4IPH)f|( r\sC<Hw$1.+qLS%P($XPFz4l6;!^]tO @rźH:Rv`h_z/?!Y3>#lrvUܭ/Z\$]6ŀhU+yg#lh݉r+.} 3mK,Fl[@s+(\F񩉏%=b-1S+oB?ڭAJ.+rl)VNfըKYsq [Snx|tq@7Smm|n.@*1`l\b4/yxm3ldlǰM$H:yс}Mh݂o=Ot[5P4j+6n%c$}zә2ھ}( <.+qw&{gOs@Jĸc[YJ ,uS3IC_+6N4 . '= .Fo?"T[%[gz^I@G3BODyRvl$`5p>uU5m]J@Z\C8LvjοI<rLJ#q: 2#HXf]ڍʬ<\6Sy~]ֱIOeFu@%AIˑiQ˵qTf5=UCd @2-PIw #G@F>-nkC74\l$y@ht[NuVe QGu)hoq4At[H5R =2$1ҡt=Hԟ*yokHT%XX#2>麜Z5Pg5qq ͒[[PH)v!˩=Wleʹ֓GG `5qAFxBAl2RPFj!zk8f^rJt_#Ɩ'ajsqFw5d}Mڴ $2U)eDq[7n2*h1L"5(-8epUƨ/(S.e!$j/#u_ʹګnj>V$@դu+$@uj0"mI@5)x} zhHT>6.6eF Ee IDATߛ8uH> BpbܡHX115G7wpW5]Zy yx^#ZI >4FpIx*$ BMu \nhu>r#bHVR~OQe6b*@9y"yPӜwPv<ړIǡOP((RF H1r#s>ieGĐYN6;:N,m[q}@[rh&qL.qF-J$p5#}I 2fzTs2+KfZ/$y0=0h5no1Zē G4$${]~Ic %Y9RPWsNf yDZ5h%ήIX$@Ԙk8qtQAc=Y tKP&yPU~1>L ^v<{:oǕT;/9 wbyh3αq9Զɭe29ɣWhw+ $P_3a$q%=- K(!8'a&%qcz HZJ\^<{b=ygcGXek͆xpwkKrʷeP=B{Ay},%$*؇?q V{?Mx_o{_5On:;;Mi' IK%iԏsY2/?|2RRF ^^YfJU֏9eĢw{(1>F8w.c2wn;s+e-m <&hG>z\L-81:[C-lH_k'q CKY~:7BQzWz*~5RUx]ݗ]hdkbqUdJ$폫9Y&s6raAmGu+79kLR^ՊnV)e2&H1xv>ﶞs,^P!?Y\6ni=]L4V51tTk_ύРc8jMeT=>&46>JzZ]@ٕօ1O0fA fԘr2n)T쌥98I2$wጳe GZı9[ϊ?+a? q\ JhYZm9b@ 16)Y+q.ʝm" VԪ(<iJuH8X_AI!!S\#+N\θu\q5b>Zq}_K*11 ^WTX uXگ'Qq%2qJr$s8̷畫<JX+ȆiyJ$<J%r?ʻ~Ԇj+m \?8F@B980X5+^jͬbR<گJk %ܱ^2b@mUggK$M cbVյdDJ"!>u5o`T=nZC5PIhZ"djUҭF##P3ݥqZR61F5-U2qs-()a2Psw^.~^E]dw!6#a:ݳG+P }ݠ0D*k}Z q'r:"##jo I0Mp"sanz1ɪHIZ|  ď2y Y2lڢ[!Wv1%yrm9*2o/s޶1ɣ3Uo- V6Ė;InؖIJ.2ՊI'I|gYHAU9\γ#{]p1%}rIؔKYe˚8m셈m$8/kP3H,n$ij/x"cѐ4TkҜ^0A@:0\1QZ>c' O*>F)}Lx{ea71u['`VFԉwXeT!!6Fm3 Peym; . q'ek@KG|M)5^QߞЯNM){J)RI$օ8mb[ >H,LYCf{uGȽ ֮={WeWuJx_\v1'$Go*&(VAm %6ի\lXb\jL2y%yPvpK6Mu7Pn-Q<%}n]~2<hp J"^@iXnЕm<(#SʠQҴMCBHoIZQÅVCJx+PI*qVP,QPz֠谍U$@c'V ,e@8a"EoOo{8ʠ29T>DfxWHU=/-.?1FbCV3:kyb9fWH8Sg6Մa'Zz{VGTI6ίEro?'KIxsm9q&AIjTG ^)zr ~OymߏFXj/T[ܮl¾(ɨ\~sC]Sb0#h#Ccj^GMܺZ-SR8s: j0~-w^ WR% @mt~5}'1G vȠWpbkv@H#hp^1*(xUcyt?;(-%^3d2Ah 5Y5ǯ qT.VƳ(Q'̱<1%_`p.+uLO + ~@_b%rl xcKɣQ ײ]a7u=̷}sкNk-8H&>ƕdZc:ˉkoVq3d(|mbD@o@b\L:ڇ1􂖻BAl2RPFŎdsr*+-~l%fsfC3Tz :W>fYLϫc?s粿BRb%PKj!-jjk!RF, S@u'D1F VTMu[ۃ`*Q%ծE{M*XtrƏ3~Hrxڒ)>tmO#֓1(0"eoX)Zo6Ջa8e$~ ɋ kMa ٛӈ龜0I8P459xb@x:mN A$@$)S/kXC/ @4p$4Fs4{6vi#Po]Ϋ(.S~ ď2Fړ>PVRmYTpG̜Tʫ^Q2rc5r[# $zMKi}LŲP((wJc;VBlV֖؎myn2ܶ𕆼ښ_xKM"L=4Q (c[cNqu?[xL ?^f:{fY$lI#7GI_ gυ}>~#yLsXTe8z}Gzޯ:ז0und15RoxZa@cx%x^IΤ+ J(ܖy0[ Z&˝ESW[[-aH0djI T2rmsmڞS2? OEB:0nlz&aΤ08r=g y1|3@W'b^=v>۾ n};wK8 U{ix}x%k|K]$ՉYkUmDN$yhɚW2ls&beX˜I9^I>:Żm<{DqkNgB/ ۢQmߋo H{c8!ʷ=[w=;('^ s{m .I l+#@$zdo*9+2- vʎRM읭wn%eV\ne%rIkn v_nNAu,IҼ;NSf={Rf#Ԡd0zI&ʰzd.6ewug}j2- nrzL4ΨS@s|=1u]_go8ˍX߫ańMJ fe0g"h2|n]Ky*V# 2i:S%os8[W-&׷aw{蹡`om=+Ip28{*m6ˆ[e58%^W䖻0Sx]U0 \V<ƍr= rʊXu&A!ÕDnR0ʷGl?տ^\n^k󩣓yk9}{~]b}u9)@lRTüja (w5sz. Is>v{Di끲xgEܫa« _ h׈)cZS5_2-0cQ ,2jAQZ Z+'!ԣyg+[>/ wï-~s.۱o/ysb/˹?d{u3#3dqXQ_3j=rɥNP>Wh>>s$8)#(tvvn?O=zr'&|~t2[?}r~F9,sǍ28X1ܥ}1z]׈˼Uv#S]3 _նZPv6~e]mܞ]i3] 3BرNn{;QpE%P?񾜖ϭeu^X¼z[/\=c,nT^i^+1uS am=uk*z^zSQs~I| |I~|=1Dx`!JPAS92l/hp{4n1l ~w ;ܭ:42 B>?ڲW,# rIx ZxQv{!Q[}`qNCǸhy\jR*o8 nkf -a3k[9X(~Qsep(uI:;A| ʰjẓ &}ɣ_`J4@nkwC#VSX52S28OAâ@%GqnkTVͶ<:b@|@HRI@GTQ:΁^aR2We͍弻F\`yq{*GceF qݶkBO2_vV/2c/S|渍H|$>F_&VJQji. 0?Im?w*wI H|$>[ְc%Z>|J=$(wכ9'L ERr $v乪#>G 1݁nR1H X/`Z(&q{]eT*(u;i:cJ8}G#Qcõrїe5@VRJvJ^TOHb<GceLH@G<$ yH~~ G4GԆ qՌ~A&6z+׹o_ GG tvgqT-ݯ0?vH|$y XRwisZGE-:yRɯk H|$yBJYWPtW >$@@ԯF8r)u9($_`atqhy@sgg'#Y - y#H@G<$ y#H@G$ y@d/HȜ9s+yӧYWWWUʥ$ y~ޭgz_ifLoЈ2t?L^aH^?G;{d{>CM @cz鋧~[=dM0$;6MT1љ¶]Nфf4RnkG{#:_hso46?I-2MSoѧz??v܎\Ǯ^]W ߻v  IDATSoѲ>;b,6:'KxZvڏ$_4y3 y o8b:t^Ͼ}x}#$I{&4OC/ƭ;??z;I^uzc4oޮ\zd5pB÷.^ IzE͵WsH 7S:Vxwvp:+:\'QS߽6nݩOZ2M:> 7K>M!.ysK=`Mhi֤:ؙߥYyuK4鯎vӊ1g {xH^IҾwrv ށtHfO˾MрijH 2NԱk-ܣc5qlIRzPkbWKtk… _wy^}U{G>g}V ,пۿnӘ1cgZJ>o'|>{Gwen=ܣ/}Ku]}GO=/_;w*W^ 8jڵkdt9稳S_~N?tmذA_$I˗/WKK&M'xB?яl2}kۿսޫm|k_-ZTL؇po&M:N8A4{l|Zpz)=Zly=:餓$Is>1{r+W[]t.~ٳuk̙֢E$IW^y͛Wܯ_7l2͚5KW\qE I[nie˖oԧ>)ZJ;w%\zǴvZq?I&G$uQkm1G~:Kr,YkF7t$i``رcd488(If}/ 6Yaܹs=cӦM &hĉa$͚5KWVssV\.=zWuwhqFIҴiFZzqۆ_|#vuPUW]}sΜ9S=/[nq}ܹsn:ٳG(.Ν;VȞ;#Z%$ci:cuWC+VP[[uy .رc5k,IҶmۊ]iǪT ӣ'P˗a=3;٩C=TsΕaڰa.͘1CzZZZkiݺud2joo5sL};n=3:O~RLFZpC=?_۶m߮[ꬳґG'|R֭SWW6oެZSLڵkauwwk}ќ9stg?ms9#{b\jƎKCdFgg'Rio׊+ HXOO~iICjӎ;$IƍӤI8) y@m$ y#jAkN#Xhy@hLs:g @C2.#8G<$ y#@G<$@=0 J< NN6lfVlgmپk^Jb%#P'A[{:@$V<5+:kQ,+ yDֲ_-Jyt n-zK㊏Na:l{MǠYj|$vR-Hw HCMKNXɣ__,%/*ѽT5;kȾkₙRt4J ۗN Gg= !$vh3'$ yL|>_ۯcHzITE4l}NVK4,"yPH9:@zj]Kz4.^YPr.qGmP<5@: n{mqb58Aj>ŵ8^6!><5/vKy JQ6J}kQ-7>FݗTǔ!yR\c tHc\F9#E8T|c2eM"Ų8bR幍OM|$yPN rkm }|y5HP˯5AH,@|5ߚ53/$& ;KG uanL &(h[c36U~*'ھD~Grk zIֺ戫ߗ%#gr05Ƨ">zJ dKG /JvGmo+p`)xE{ȴ=IzΫ&m|> D_o}z?D!X]h_k31oϫ^P.#yVNCc Hox!#|>BAl6)7%.a/Q[N^퉣3IJ x9_yBր *^r[luW;tĺNH(Kr-H*1sa]z[s9hXYJ@g -l1L "M!.T_m]OMz^W RV6~HD4(眏vQcdkuچz9_2l3ѳ\N2MESQ@G˒ fJM ۽ ?N+(I}.Djj c%s[cR3ht^s B/{$mm;˥?yR ħ.RU: e w=R%C|lvN t[@ "KJ]RW[ZkDwg$@ɣ3$$\*0$ ycdtU/Zn%YIa> m3Jmqm?g??L2N.Jwt)Il]ۅBAlf_Pu9;*)d=?ˍ^0WpqܶCO`T" rV4=j%hymbr`9ɷNHQ@@BBKҢ~yۻez%EE:Hwh$;=2!@n &Pүe.:~ ) %#P,]/0۶'Ae*gjAd3Ty%xn 9Mc=FN:kbp#d*Vjva(gdcVk2nV9畗\[NvVrf^9Z4{7w^BaԾx^yk:;;MiQ ~=CUov>7ac{_aJ؊iQվQ,FG۾+Z1{}} tهZt[ju ےe0WRGm snpMZ$zJY$~ut;uCۻ{aw.S鷯Cmċ}$>arZl3]h l1#$-:Ӝ[Rf.6G%WEϙ$oOHrZ8̠q~ s&yt^GbH:07]9wn˾~&I ㌹n7Q(e*38*>v$MkDo;VRk˅~_.7}utk|m% J` J٧eЙH%^^]Q[ϷG8ǭ-1m]aݞ/+="&0-xn̙(u1iߎWtMpv 7J!vqmĐVG%&aZtULݏ-^Ot+VcMlXJ:_g[MRI+H`0/{:Gf5:{uy7L( S(!zlڟs趽VC9\d~c.k $-&-#2>G>^-^]~~ Aqp}蜪 U5FGc%]}>@pVɨ]n-a:yP -[_QD1a&5'yи &$d8RڠQ.M@($qN=8Zﲎ-y$wo1T,$fWIL 1Q_K 2 ȃ Fh$BQ. Y"!$$l Y0R9uSϩtfTuuׯR(QRvo\-sL&yDn^"څ9kK `a*307m;eㅪ.l-!QH5yTKIpMf.:Q1t|$vlPԾ,'#5>F#cڶ- P[gAT#-+(!42ߝTI_yQDDFfG[sHD@|OZGQjݚd}oYSv w=R~ k7>}gq^Ǧ6vwfE퉏s!PטP͠ȾwLG\؉%. #T=!Ͼv]2;qbPX9 ؉F׳CǓG5` 8zU]DhuD_%ƙb_(yT , CG@q%;KW0 gXu`J'ʐ<GD;H=3_<><X\ Gf0a+Ao Gkp$Y" )<syd~ U #zY JT=F5-Tɣ%C 5ɣ8S#״&ywUu^sh/ Csғ Z_]$Eq(Tﻖ@&x5኷>'0V]'mo\|p,//GW1g-D\nhWceYZZC|ݑ#m*'doBn벵UCƲ&{TZ$ꭅz%>24~Gנ 4XL~ r1ֱL7GG#tEIfe(cߴe vOЕ%>ˏ= r'^!Z2DW~(Y)d &1Z4ncʣɣ:Q-ɣ"@?lbW@iyTO!i yH/ֱ{bk괪 XhJ$GR_udIl 4[ЩzD$MEhW3' 7Ar*flAGGhfrM,KKKQc϶[kI =n٦rB&m J.H$ mM#cޒ_92<ѺHPKH<Qfn04C< HУ1I2ٍ1i%|<Q֓ ]>ZwB'fqrlc4NN}Ǣ<G["eKLղ1DWiZ-)TWOM'$ǙqYu|0 IDATP /$`K߉3z>l-H t7rk( k9v$]񉮤wc]5+^u$H=2L"- %s+ڔl@NnKf]$H'QT`ď$=҉DXӧ9kW2}񲡬MǓMˣvU2\]/cac}C)eg7Hnk]ۙg=KֶBHь|[1&>|Ր?KKKQc=80(qk^%Y,dܽJka%Yʕ9~Y\9&6ga*'docQ嗧d(VM8ɰAuNRg<$Npɵ=45վD*F5-+j,H+3?lhd>-')wK%MSS1=F|9Y;+ZfU+))|XlW&ޣ*<S[NJ~Lϳ%VASY2dӧ[4C<*{|K&JV5eeXktjrX(Z]E˜G;iI6ܘG8Aeeۦ'}LO5_@diɿ̐˲ecE|Ǯb\Y&w%}Ӳϯ㧯hz4:fEc|CŞe/3uyeuwu %yljW2)46J6[41x+ Wfpl.1|c FM `U-g=>I4]k]vjL6hd\ӶmvlIHǞkyB]1GVsȲǽ>'QAx0t!N>9Iû}se\ӝܑs:g>ٶZPtQ&3c]2^vTi]H/(ۚ."R.1r(eYsC8tOQKmjuP>- U[r,*coj~>\Wm˹u.{s呲gVyi>L<;׏%C7s+Lq*w:|Nikhp`̣L$DEg&l1ޯUH1;;9kS<'!`$NAtۚNqyB<34. 5Xv|uW-{c4my3HCo8V-NiNlklklsc+><:vzK&ueyouöB9@-u3U~c+1O~?5n=^5VƎm.| , U˯k^§|i7(ֹ\)eq rq2fM$S|c$[f-:|c4|j|1>i)?/>cX}Φ6.X|مHu Ǫ%M ^G>DE]MN}9>"q$vv9nv1^6=Vf]R JXID RU[Ucn{@$V9Ǯ HZq"b%ںV-d21h4ݻw‚۷O~˕W^)'x|+_:KDD7,ws=WZ9s5du/\.RY^^$Ieea׮]""7I#9rDv%wy='|RDD;8ٲeȎ;ϗ~Ǯ/)ǎ-[͛'c Cstg}wqO͛ljk]7o۷?ϗ @eg%I"ѣGennNDDow-gq<#8iucO.Ow}Xַx=:^s='<|c˓O>)ww^ټyqN:$-=>|Xo.x;|{ߓ}}'o~~zb];v|;eٷotIo|Cnj,{ǎ ݹ9ٹsD=묳`v-=7z-4Mȑ#+"~O(@G1@#L־u hEcU6@mH,M]]/weuXjUv?C:v]G9`?ufI"p!c϶v}1@ZNr qޟF'YwKvSa @3d&"F˜H$یcUֳ^]}`o$i*iG& h,an6UR@cZ;{]x}XMׁ3xt]< uv, D|lh<rbM}z]g[LA^c{/ w􍁾kpc:<R<e&g.s9}nn{n2HrqP]GgGG xSe MѶ Z[WE0wLNBŵVyYTy<͆ Vm 0跲=)1bYx5/GGRպmcL몵n׶L[^_K`$ cLcBƝ]]fjD| \]s-c[|55jXgJBĝ27VYfzIR5I\9P&Rt[=$kZ&Msc(ty!I*~\M^5}qVv;Vƺ-'- .]?7e|jU_?m9$hR,[y]ǩuEo0>KZ?κ dM$gr8K/JjgelMX~׺.O* Y:pyGI(Ol(:٭l׋k2ڪi%уxE \jo@jIwUW!CM{y&\\Q/*ɦ[C#Hfyi[*hW@VhJ({ŏX13FN %!-G͹ .*oďp@xP?ʔ#L'lə9F"fnj~~je'zNۧZ5[ghV|4Pf1RHQɂiLEeeʞ6ۺ]kkfu OUg<Ԟ^ `$䌇4I[ՆWdolh42Fm۲FJ}^&1 0Xl7C, d:t>gآ! غ$/h4]|T:>ˀf[ rgUW4I5fp"fz4RMd$k{~e>p8l]mfʞ6ˊLW/ןx41&,bY+~j:dEisqqĈi5gwWʼncN m(4.G +1!:{E'2F'^`6> @, "IbPWiBUg[zH=qlJͻl'*?2[&/KdrhE zU9m$Yչ r6 r-j2$ڒ@j$|r 2#fRհ`0xUkAQ'ޛ2"(A[6*#лHrfWM\@Dq:tEݵ}/m1.Pk)nih'6@m.k}VPLgRLԪ9@m V..2fID+|Z <H t#@#41IIV, Zu!#b$eLXAP`J տ$qȸGAiy$y3q/@^b8pC9x"\{[W;T2@ƌ+e#L-Yn@#Р/F%ژ ŕMCb dk>326kB|f4e;&SI21/"cFOk۶Z:ߣc[7ֲy8  `~008MEDd4 c$e# 4ȲZRZ=-ŰuVhn"k7kc_LeuXC+L++u>82>|U,bYh{h[Leeee1-V^>?'kKDZGSMq111Sʠc,*q,F40NEndemztq-&9^>+'&tʺoڽ^Ӧ+„)wb-8f:k1@x64!qb8v Z7'k'[%k$ɸdJNia׼֦ɵ$*I>ԍ=VJ0Yvc*Mr1YdZ/8kP<}[⨮הZ%@ƤG g,Z˥`Жܩq.70͟cjQTy =&Ī4IDAT3o@ASOL>aQX05;0k=`V3qW\,[T*d@|q2nIM6>l.O+ζ6^_뭃MzDQpjr8ۢwQZ0hG`FRmzZil$VDeJ1}q#̝MG]17&ǃ߁.1_dgU4:u,ԋĘi9좤 6m/rVЮ%G˿1w.2c;#[vbG%OnllbѸɢ[tlh/gdN@Ez3tTuK:&ͤ1R=;@AbEz'(4^ulwJ5.lή sֶgohj"2q?2m m86ns]eˊ9F.;ic]YGC߃=lQ :BA' =ŵ1FSG+svG`:D}D2#Iq L"xD#ޙc@O$zj]G&x,C|DO<:jnFB0 yc5$@7E9ۚ1=ƄoeepH€1־ Jϋ""2u'>` b`Gh4JM7Xo!ףzYmp8l]meˊ9Xv1U+N2>"ٳ]ECoT=Xm25}, VeeЅN-ڔyB! 6XpiŽbp8'#H$HHZۻf]ļа/ڟ,\O=<@ dvz_OIJjIENDB`tsung-1.8.0/docs/images/tsung-dashboard.png0000644000201100017670000007460614377756736020403 0ustar nniclausdreamPNG  IHDR 0bKGD pHYs+tIME .4iTXtCommentCreated with GIMPd.e IDATxwtuM$"5t) ]`ź.ۿ_UKT@\,EB@$B?8"a|9|ޙDXϳ iX4iB%̅0B?d!L0B?d!L0B?d!L0B?d!Lƥ7..NwUAq٭lթSm6k7sLedd޻+YxpT;ƍ[ھ}0WCuU6mԪU+\R'NեK5iDuUQQΝ;͛7+--n_///Od}˫;jРAnnn4i$ ^{ګxէO5h@VUIIIo_WV'''kN;vT@@u:uT_Clm۶n]YwYEciܸFSqq1glРAj׮e}/~Kٳe'Nv%Iܹׯ Ԯ݀w}bh RFFݶ{1999iǎrvvVΝU^=-Ys:tE}Qrrn*wwwo^-uqq#<@mٲEiiirqqQ۶m5~x}ᇺx"?w{jժǏkƍܷkח}oPFTXXSNSNN+z衦M_gΜ7|#GݫhEDDڷo6m_1YTTp-(66VZ-nݺ۷ի:uJ6mk/$$D}Qzdٔ.vt e߃ٳgkСjҤx W^jܸ|}}u9}w\S,:dWg*//OaaaƺHIҁuTLLZnmIYYY:|>nݺӳ>ZjFYnu7<<\v9rDEEEZ]:x]f'Tk[oZl+W*%%EҕGn]|YSNGMff]slذk5ҥ)))rrrRnW_Qm 6mĉU~}uA7?JOOÇղeKnZ裏䤱c*<<\ǎ1c懛} 2D[Vvv~'I&ԩ܌b̘1:v옎9VZe˖G}d奱cرc:y5jN:qZpJJJI&fiǎ(M\{ѣG+:^uBfHU֑:+Esssv*ͦ={Tk[o슋վ}{]xxuff1׿$go!Ijڴz`j׮]ƝH'O?88̴k5kL111 TvvlRc@_ZZj|p'''Ĩm۶+!!AVշo_y{{ĉZn k{UIIӑ#G$Iiii1bz/Zc(SRR%Kfk׮ Ѿ}pΝ;ܲs)??4j(l6%$$}}}+|D~~|}}oСCUPPP FGH{U~}Anpp|||Y~ig⫯~?wի5tP׿ӧG^.^XSL1&1BՠA  2DG\e*;Wڵk={jݺu:wBCC5psvC iذaڷoΞ=jQуS- QQQjذ-ZT8{\Gst 7nN...ׯl6]o)**/ooouU\]]wjhԩS;wBCCհaCc6Zqq>]tI%%%FSj..X,ݻwL;iܸqh͚5m۶l}Æ ݻ]7wRYF&L_-Ţ-ZhѢEp$iի]ccrõ\5~eצ׆IO>Wzc׫j$mڴIΝ$ٳG:u\e5\w}ÇK2[]Æ cʕ+f͚u*--Չ'a{jdw{eY@{C-{xxO>.^h7Z G~ fKI2,''.*E>C˗/e5-;vRj>eʔbHӦM+mݺUv**77W^^^g}Spp͛7xEEEvi񦤤(>>^zҨQm۶O>vVcZuE]xQ1czQ.j?G Fnnnj֬:4ijݺ f蜫4`[X'sΊm6;v7Z,vȐ!2Wb*Jrrr2c-zzzzkܲn_NN f|C㎭9veW̢&sSP{2'OkV?_.^{5X:^u۽{wWRÆ um۶boI/^(r ~~wB߿_="## hذa:vK&899InnnR߾}%/ҥU^=EGG]=ZmڴUZZYb,ȑ#ׯBBBt=h̘1d2TXXׯX%&&T%%%JLLԠAˡkʲmܸQzԶm[)<<\}Uddd ,‘szuJM>h*ϕ={o߾j޼մiSw}Fv|g9sFZ҃>T5lPvϬ;}5b޽[jٲrss)v]vM6ܹ|}}#""'www޽P5jH%%%kw_3gԸqc 6LNRHH"##URR"___5o\GqթS')==]5RFj|<<j֬u[NGVPPQ2׮wuuU=ԬY3yyyZn&L ͦs^w,em]l jrrr/?V߾}ոqc999Z~Zl(YV͝;סZ8:ƙ3gJΝ~ֱcǴyfP2$$D}5fjÆ vpt 럫zƍ^nFY7b׽6JX;p@E#IIIZ~}$={WFDD(&&FuQVVNt(aaaQjjVVUn]?pYrssmq k#GXᡞ={*&&F]tQ:unOhݒTN|VU;w7JZhKqo+WJBCC5k,u+hӦM>WpqPԭ[WsQPP@ׯ_ 2DFҫoF.]}())я?(IX,z7չsg[wԵkWKƺ5͛^zI]vChh^~eyzzJfddpn;njΝ;+>>^˗/իte+VH2СCwǿx%I7%mڴ1jT/_l,7k֬}On~@-iӦW=;vhΝڽ{~%:|̙z衇naaaOڷ&όW$I~&MTFƌ YZh-Zh„ *--աCsNm۶MwjteK/͘1fŋ駟hѢ[}ᇒ_ںu~u J IDAT]\;gggiFmڴɓիWkɑ$}ׯZn}u>}ن5ԩS?j߾}ݻw/'{ԡCC={7'gCRAAA0a {LgϞfӒ%KpYYY7o>SVzꥯF] ,ЪUogٳZz\]]5tP=ӷ䙈TEf̘BIқo)///??z'%IG~dggkԩ:r䈱aÆС"##հaC5lP!!!X,w,sdF#F(99Y6mΝ;w^eggKj*mݺU dV~@-riHVZUm۶5n~vZK0SNUk?^ǏjUbb֬Ye˖X.\[o/ʉGzm۶Urݺuoׯ7g͚nݺ]7+))-lUsJ(%%7 ;99)22R>^~ecΝ;9PtX^hΟ?_ϯ\Xܹ ܹsrv*711̙3n/--տJ1Bӟ*ݯk׮ŋ9P;V>>>jW_}QFرcv)Wq~&ᆱe˖}8++Kqqq3g+=FӦM%I:unjHիW=ïΒk +,,ƺpB; EEE7ސu??`]|YԠAEFF*??_gϖ$9rD/Em۶UHHRSSu ͹]tQjjN>-I{ԣG3ƘiqwޭiӦٍ-((HM6?l6yxxh޼y|>M0AfRnn$)>>^III?fϞ^ͳkք {I<Er?ZL0AZ$=Z˗/vG裏SO_~{G9995k HX, ?ASk޼y $=33g|I'jȐ!˗/WwP\L?ͦL͙3G/-ZTgVku 40ݑ}O>M{VkW= @sYf4ibbb$IgϞ>c=$B?X, И1cZrrrrrrϖm;oztqlXw*ۻ^U\\_]%%%ZvyYFAAAF!Iŋvuuu=$r˓tV_IZt222t-\GQ\\6lؠT%$$hX,_U-}Y}嗺pႲ*///IR~'Nh߾}>}V\yUwP˕=NV1b}]gFɓ'+..N>V\gyx껇f͚_FҲe+C4}tkʔ)zg3f\Ud͵Q*00Bb6mRhhZha[v,YKR *L?5ѣG?Y*((Џ?yiȑ*"5ҤITPP^.]RxxF#FPM^Pp{/`2 r/hɁ%JLTP Nj\ZS_x/7nB?)4o7S/G:^ujt?XB-_~3):;_g)(-}$L!})i~@ '>!7g7npYdmЗ' ߯4mON1-AAAjNˎ,!H jŃ%I~)a.sV]GW_&HaMa`2~@ ,?p+&CN[٣ZܪlMc[&yxі~Zrssm|WXXsY`A<]<25%nڃ{!L0B?d!L0B?d!L0B?d!L0B?d!L0B?d!L0B?dZ$##CfգG=#Zll6[fSiiMq)EEEq"L%裏*77W`=Cz?))Iݺu \(P;̛7O[믿."Ijժ<==;hܸqrvvP3Zb/~a~e  fee/ﯡC7TQQTTvء3f(&&F#GTBBKJJAiSII$j׃>~^Ѕ $IǏ$>9R111z'tȑ֧[]1c(::Z>V^P_e7N/=nP dff*##CM4)M 4˕RSSW_UBBR?O˗[nx%%%i={{͛7O/Ћ/wyGz$IK.$mڴ/^\K.?7\ü<͜9SÇ'| &h֬Yͭ/e~mM0AU j~EEEZnfΜM]v6mk&LVZnݺ=z.]?\ԨQ#mVc͝;W͚5ӤISe_4m~@-ѧO-ZH۷vZ=zT5,RRRtJj՟He®`1++K/^$̙3FXw瞫v[s9VZ?~:YV|_WII֮]^k֬QPPP})p{ nj3f(11Q>l٢>ٳ5eyyyM kǏk߾};wzѯ_?ĉڷoO+WJ~_޽{ua:vl 5j̙JIIѷ~{tف:p.\hlX,zg_… ΖKEja͵Q*00{*33S 4?zȘݖڲeӧMҕЮXB5t GΝ;ڞdXԿ=rwwWqqϟJ999ܹ~ߩnݺ*--ԩSu!m޼ʶV.]+W*==][ք O}ֲe˴h"}0aOnvZ͝;WiiiWz)ݻʾTTf7n @&C `2..^ВKBk!L൶QgbXꪀH.^ПJ(`"l6l6ŋSkt Z`Ɂ%~ RgR(33SAAA5~@-HYfbHJJJj| ZgT?߿UZZZ{X,㿚L{N!6B?nmݧz(~`))Zu|Hb```2~&C 5)0B?pwr"Z$&&ggW]m1VGwU^IL?߿Vl6|}}k| Z /\/vyQ~TAIEL"?#'''y{{3,LnBaa)1x`2~&C `2~&C `2~&C `2~&C `2~&C `2~&C %jr^xAuԩlZrvvmP0Ez-}gϴrJ:}^y$u֭Ʒ @ML?W4h`|gyF/Drnnn 0.))Q\\ *..N%%%$ժe˖iъԩSaSNu/I 3fjՒKzuݱDEEi˖-7n/^,Iڸqƍ=z(66V .4ͪWiZҥKJOOWzz.\wСC}㕔k5oD&LЬYKJ6mTio&LXeggk̙jҤ{weY5 0 Žv@MKsMKX%CeV.uN$O“k[Lp7%90q8|><纮ύyuw>\yYdIlڗ$;v׿uЄy֮]\x\}ܹsF.˿Kn|k_ {葊?6l~UUUjjjRRRN:ϡ֭[oq#G#HTUUoΎ;֭[gҥI7xK}9r FMMwܑw9ɛO]re.? 7ܐ$Y`A?i&Iꫯo߾ {Y[[w!W_}un\uUݻwFÇoq{Us˖-`\z饩l[7>Z*))Ir'6VN;%IVZUK.5kV_/ڵiͻ+ mڵk~\z饩ΤIrfĉ[ܯ-Zvڜ˲2wu&T`k~ĵn:k֬}Aeܸqy饗2cƌ=:wygdĈ3a„̟??rUW՟ۮ]$o+oٲeyg<8csKJJr9ŋzh"/[lVUU[.˗/_$Yre^sS{[nŅ@VRQQQgLǎ};`y[֬YÇgӟOIII>{iժUs7殻ʊ+ҧOtI9Ͽs 7d͚5s=sie[T?I&M* .Ln}-x`jjjկ~5?|N}o~믿>[WL>='ɻjn}s?@c$hk >*B0h|,@X PS_ 4>f@474,X)/|!ރVZO>9)ޮ{om {W,^Ԭкu}v5vZ MM$ښy͍B?h^XA{=[M{g]ͺFF4!Grtzi <&YI=`l3;߽M`Ψ֯S72]ʻdњE[Үe 40uyjSQF IDATf]뿱I. OHDzz~{^G:;)w| oC@\EM /o^8+-K[6~gO/)_WsKw嫃$9s3x|c7rsf۱ۖm랽.c'=v?#:dEԥEg7,Z(7ߵ$%9krKwg^c[[6Sּ,eRY}ruꗫf\$ɟ_s~ѥKy푼$ɔSrGovK~| g]ͺZ>+ͺ-+*WyyNZW*+rKG&In<$Ɂ;{f?fҼIiߪFkm6_/+r3m44RB? emtQk汅k5\匁o.ዒ$/|9GzT~4oG^{dվW.vIxFZPy4/iz3|?sJSRZRsںڌ0Af򫓓$;%?zGY_>?=MdSEk劧HrO@"i@unpI}O_^K&̛{ڙgܧeUժL7)+S v`NsrYL}Fz;jutۻUy  k&Inu{0,IRּ,UUIL]05ԏjݞo&X|kʷR[W"FJ@5`ש_n+ɛԎJYiY04~%IrR_JӡUL|eb};.seź)+-KYif<}iY2Z9{H\23=%eҪUNsRyw=3̬}˲x?fv;oҙΟMfաW.┖263/Qz~y]m̌;p\W.τ2}.ra};Om嚃io1od~6g)IIXDDnz>;p\XF&Νa;KiIiJKJf]}KS[Wfk9cWdM\̵ˈƧ0WYY;66nP\v G^(42D-MM@ |X߳7B?hN{Rʛʛ~'7vz|@[Ã8xZ7o~]|1=;l~@ay/ F#@`~P0B?( F#@`~P0B?( F#@`~P0B?(41wuW[|N]]]jjj_ϝ;7*5>,B?hbt=SYYE̛7/oveԨQ)4!-?.(ՙ:uөS3ƀ@#%&dĉٳg{ :4'NW[[믿>#FAoYxqSOM 6,I%s%4xSO=57pC:ƍˡC9$ƍKuuF8xL6-cƌsfWZ.(|p|x:̚5A͵g9gĈ{Y* BMsG$rHN7x#Irms~+LEEE~$I~$ILGqD綾^ٳsG'In̛7/^{m.<ù7k6GXoرyr5dرx93&|~ӟ6Y&w^9wi/8I;_ FOMK/Yfe}ҥKӿo[;ѣ{_~|lݡCf>}z7ÇsI|pȅ^w9{Go䡇dN;-O׮]>˗/OTUU{y睗^zeРA93~$wܑ}k9#ңGr!5TUU&%%%ԩS?L4)[No>#Gt1574~ O81#FȂ ҳgݺuqٺ-[!{7C s_`A?if5{QsiiiK,I]]]vim;S.\$Ypa۠V޽T{:t萫:v[;F]@lU~f„ 3fL\e˖K.yWCW_}5zkonGo}[9RWW`[A[AݪUdɒMklÓ;w̟??[W^I.]$;Cf͚ן3gΜ56՞kfKS]]I&sĉөS'[ { 1cF^{z p)++˽ޛ:*/SOGƎ_|Le˖m^{;6sLʸqK/eƌ=ztՏ-[3L~dΜ91cFyI;._~y&Lo[~_6swߝŋgiѢE˓$+W-A0~ 4(ݺuk,x`&LQF΅^3<3͛7Ͽۿ%ys{#Fl?$͚#+cF_r9 0 cƌy} ҵkלy92|%'pBN?\s59S>w,_|qwqs%,I|7@WRQQQgLǎ h#@`hW,MޔVJAfҧ]to׽ѷ{޻bqf >֥{+߮Q^hnz&|֬3onA |X߳W5B?h><,7&#Wq`yYiY`>w?y/1}%)Y{_@>۶6\}չpȅw.Geᚅylc x_~1hݼuJR b {f3㰌;>u?(/|9Ir;r;ꏻ[2rH'fwO_u^gP~1XY2cqf~P0B?hZ5keCRVZ(&_~>l4'=) |@sr};K***|\UVVcǎz=<|M[\U{кy/_٩go>%{`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@ YlY.s19r 'oO]]]jg]]]jjj>PgܹO/Ygܿ3ܹs3xՇg={~7=t4V'Ϲ+_Qu޼yw} 5?rQG=veԨQO?_9sfڴi<NȏF(4 4 W_}uK/4%%%Iuֹ+s)@},X<_|ޖN:e̘1_>}$IOS  L>}hM}ݗ/| [>WU6|0rJnƍnθqr衇CɸqR]]ݠδi2f̘ ><{l&O$9S$Æ `_j?N8!tP7xqgyf _)8 vX~_58w9gĈ{mo4a„ 6,-[hVZ.(|pKJJrG|4+W̲e6oiٲezͷ|W\N;-vFz뭙7o^\veysW7sf}7?$o$SLd;n\|ٳgK/mk7GIիWˮ[o5g}v,Y$If͚wy9c.vZ.TTTlqGMϞ=_ocǎkk&cǎɓwo:uꔒ̟?~ܹs|c=2m4K|>B?hjkk?z#G̑Gnݺmt]wݕ /0;sc|C=Ԡik׮?iC= 0  wL0~߿tc=6+VHj*7|s.#;SJMMMJJJҩS4iRZn}{sֿdžTUU{y睗^zeРA93~ oZl|3K/˕W^ʍax]_BcM@ǎӱc̙3'{w}+VȘ1cr]n(0իf-X ~xmmڴiG? ?뮩ŋǍoٲe,XK/4ݻw:tW_n-W]uUzQFe[ܷwq׮]7ɒ%k NYp\N\pA.䒜q֭[Nl'F+XE*++ӱcGhܹ?ӧo5kV;K&cƌɈ#r!hӧO~L:yܹ7o^>O>B?`SVV}gjqM3o}+͛{^^,eKa=^}>t> XwMŹٛRY[i@ЈEСVq+#4+{/kjDV @#QWWdɒ%nRZZڨ,&gomeZhaB\2:ujA V0cuuuo4'lJKKSSSGWRRR[f%rK 綆^`sXnLm 392f[=g`@`~P0B?`5rH AЪY+FF4:3ե.}\P|'=)Κ5c+ {ZmmmҶmF^4u <|M[\U3(\MMAhd5kmfWRQQa: |ر @`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`( }B?"`,@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#ԾiIDAT@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F#@`~P0B?( F@͘1#<2SNkw}ӢE >@&&o߾y睓$V=ܓN:~yyyVZefժUy駳nݺz?}ݢ{I-rᇧ6SLɣ>C04ZJVB%Iv L֭[ot݇a}9pܹydzrʴl2{b(8$Y~}ٳgϟaֆL>=SO y3tt9K.C=:{Δ)SYfY|y.]?8z&O}6%MI RRR 80#G|O4hP:w6mdfΜ}7Kڶmw9'6ZgvJmmm,X$={vzYTȰaҡCt%_ϟߠƞ{޽{M6[ܷ̙3'CMNҵk 2$gޢ~~P~ӯ_~9RQQɀ4ysO??޽{͙2tдo߾~F`UU $eee9ꨣ2sL6-nmsύ{׶/3}UVSN }+--ͮ^z);c^|K}W^_ iٲFoi֬Y~k.[<#FhЧ}'&M9k֬ĉSWW#<2]vm0W΃>l.t&B$ISSSC9$3gN&N#Gl$Yrjڴie˖sۖ/_;&MJ^$=zVVVYYYؽaܖ-ߧ\>4\jUf-tҼk};ͷ0aB:te˖^?>}l_=K.M޽t,ZOfW[[[.=X|hˑhrмy>Oeڴi2eJڶm>8͛7Os'O<]f}_-=`Gɺug{.eeeӧOe˖.ǏWݳ:+eeeo2y;ǏϨQ>ԥ4.%u޿t@l+{̙~:{_K{,Y>}zgp#`y/s=S]]I&eڵi׮]X @ccy/ F#@`hhڵyGg͚5i۶m ~hQ[[f͚תKR[[f%>y2yqњ,Y];;vK־Z5Fw)3ަ2RUUCfĈׯ_M3f|(bŊ\wuK˦߃?&;G욫FϜEk2gМrM_GL>=m]=m;wN-cewf_Ŵ++u6JT};忟X{N90;u*3h4 w/6 !XS֭[(x ڴi`{ǎne˖)))IeeeL)Sd̙YfMuV^{֭[LSf֬Yi۶m:t[o5y駳{'IΝyGϦ&ݺuK'ߟ'x"-JnҢE ZbEn?ӻw5O.O{=C9+^̕%g.Iͳvyeٺ{œs?Afܽ2y0sIo*=:.O//)'J3g|-yύ7IkKy9O{-+TgڥYII}{m&vלQY=d??8̥MyL$cy)=Z:n"_NY*֧c}{&a꫙t]ީ]Z6ov^W&eO,7gP)mVں/疇*2{۔,ݢ~oڷ!j殉-i;}z ߺuvt]JKKӮ]PoԩYzu:|3ɼySO58gg}rgw<$9c$_җ$UUUұc{f:uk%IYYYo /y_¿|@ps/oKǿ5U5[j?9~9wӋ2jR޲4~jQz.9O~9j>СUo*)3gu;;/-^뼍#I~~߼,\UN-Y{=Ir2q撜.fMUM6M|j殉-\w^Mޣ̙3'CMNҵk 2$gnpܠAҹsi& Ⱥu6X42tдo>۷Of$?| >;w΁ݻo}eee4h6Icyw1z >}nڶ{uɊ5I;_}:fr@ g[-X_hKCwN2[|S=2 j0d2`tmrި%k3p6K~5|3=]ۖ٦Uio*o"u gwJɀCv__v/,?3tn;[$Ӌr٣{ZoKҮ֦>nn6/nݗ6-OޔRW?::)?}lm^ycwog>w7Wܐ6KGzJkxy`˷dwW6xN?.]vYx≴; ={vMֿݜ9sx,X 555w}wϟET*e̙2eJz\uUikkK\?f„ ;wQJҥK^ƺqҨ\y3SW.jr#f TRuMMrBsRnwՐyiؼӛϮrho717ÑG*|s[LUU2w\3?l.ܻ|K.acϳZGW\e3G dO::מ?u=+3*OH/=!=_d߁3c8=+s(;G-\sNkLÈw=ޑ;j߾}s8wKLa&WvesWhɃ䤦\<Ĵ6Lסm_F(e/;W1];ۻr 4<ϝ7OPmRCܳlK<=kOU13oz '8{|=U΄us?tk9&mҶks_{]x/AP0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0#@~P0_`v_'*ecIENDB`tsung-1.8.0/docs/images/logo_tsung.png0000644000201100017670000005222714377756736017471 0ustar nniclausdreamPNG  IHDR}; pHYs  tIME "&[kMtEXtCommentCreated with The GIMPd%n IDATx]w\gfe{M-]̥K.rI.9K9{L11  ,3ch,,,Sy-y$IB g*TPyG q wq*Tjt*"RAYƘq/z *TQB;*TPyG *TQB;*TP *TQB wTP *TQq.? VKg{/^rEf؛{_&{^{]bkGO~r3/I(pHB̤/xk2xO$镘)<rZϳ_~j(`U{-UU͎P]Ic8Eݍ\Ͼqz/i4|9灻?O5lZőϾnu q* #6#'nT>᮷MHVђ͟C1-9cˣ vn3 h@CS$8--s-,wPׯZ5"Kp pҝ5&M/n,=y]َ2p'M=)ݖKdRų9{ aҸ$HJWRNDILlvGkcdqؘ(Sx14 u&@Dv$b,55qvnw5]F?9kj8:v1}X1wDu\l`3w]3LJGzv%Rp{tk%ϛ#è9OnoRy;bbV5w*o{_8.u/}~ꖃE7^%9{H7(-ɫno^q-wQ_ⓟrߡ2TNsa6Ԟ8?^>e{k}92|̭+f GwXJnuy# Ac|hM,)8ퟯ7l[9Szڌߟ2i|?=Yv <}łEO܋4t@whk>)l`@'~rCx=b<h*5%6?luڈ_?[v}ao7%(4zDohb9ؽgmXk+!qn̹?htL'/6MDq|sp÷^^ZZQ;*-lTua% B-#I`,7%_fs8X27TII H4sf^]8Ua_`ZaWo~9{]z<1p~pWLjur+џuݓ?U?ƺ`=7̉w]: 5e{+k 1GZ. b6<"m3y{O1S2ZN2>QG炇jg⇣/:ݖ9w^Kk57YǍIG;N[=45tßngZbv:()Ku(D k~+?rX|$a|ګk?whYZܵW]|JԸKx+s2]׏L1 c/ᬳu\?&qÒkvrl>:lGGV+f̻o~v̻WzZ]RZ睠@݌g%LȈhi7" vפ(iomiqz$~٨@"FLTC2,ΐ z%,*'8((bUo=ޢU6|[.dNKMi@ 3Jr %>.s,piw8bH䮽ݫ-&ȏtr;kD Crko-db)EYOCXBͿ1ٳO0!>C!aHJ }3=˯ `ٯL_.1'D枑0Ќάyn.FSmnq0,(5z|?o_|.Nn3&Ƙ#:[n֓y!aPW'@י_y{bLaUya4tr[^LRbUlvբݸǿߜ8cV;afK\bTCꙸѩPcv鈦;6V)*X 0lIU"PIYcMA&wLo8O[CBb#zr[Y [—-tnW'޶_}9|$e8r* 5) M-  mVS7ΙS[Lޫյ/*l5Z [:lUܞprn31~3*-+F^|Q9o+ɲ0~U>)Os]67޸bEJάiߴE|c-3߬޺sc3p@Tgt7*UN4XC6tYSdNpV?qՆscǥ)`KIy¬)]T쎟<[ܑ1D@-M$H5izݍ6£vI5<|7ڶ K^-/ F>\K/y"m^~kÊU)%;˩9]]]x9{P%8֒eݪ]kՋ%% SUg#H$2gUuư3Ɵ:Vf4hᰏ_W]~t:ݟ:׍v݊Qd߂XTuogI'7<1^ 4uFs(fhoa &JDpJ;{쵝C'ϹkcUq]AaxrnK_i}ks5 cOsǴsM}IYli:QG*Jc(Y 6:YĉN k8Kq. w7t ;vYN4ũ?S^OY}Tp l6gdWV]iCkXV\_oSC؊M[w9A>?\S3&ݴ|?^yzh<-7dYwS'}!gZ9&䗜}~$)8XtqA;+%/_+ {ńY{ ,x ]ZĎP;&V۸cZV`Ld/9G_^R~80sRc S&$N#O7Z~䶼Y5{jtXDӧ* U" !ű+?:͝3&[֩I1'sy=cs ,UyhҴI c~wW䚄YϽ-Y|}EG_P>k+)~ KВ5C%Gk̙z諯}ݵf2NWeeއ$Q1s oz+˟mu̹}VY+iĻ-@dRsFcg/sW9%0Gׯ~7DOXqe0IƤIč8G,zכו4dܰ酪Jx{/ &]>?$3co0:z=UFGLX=|7c8g_S" dXRcYd7U:fI^t QKw&(äϿ&_d[ecEyvx=LdXe-gRҗZw9J1L%3"\RnCo0_\7k\ ([0,)bqW1v;@#Q}꨹SrDqcG@kXiڝ7tZ^LX_ȮO}YQEYU{ lݩ ˺v,y⾍G7Ȱīι-S2 q˯VƠ$ {78ye#nyb3Ιv<_77]I}l~n嚙w^m̸;:O0yr{&&[Ѐ29w#EuP18⛩W]騬ZmP[V>_x'joٳ ~F=uŃoE(QŠlu/yD.]|џoz{kFFuGj/!k^x~ߟj!u¨]8kY cމxoǏF Gף>ܯ %,^8f*qB[}4Q>VG;gf,\8͚rAy"u Pygh>&'FE\Ȥ.{dܙsJ*.6Pz+̿?Q=3?>>Pm * >Vޔ(50kϲ)Tz֠AnY9a]p>eniqo^}ӵ_r 2 9Jy)9_sD*T8ugʢHZZ (Jyy_;u[ۍU2#GCb1}Z zW}?OSYQѤK*PD.*[3Mȭ(wX̕ pl'JAuu ox`,^鑑AXV_t_ M* `ʔ{8%11qgAy=,I6m)~rP<|/GY-MTO>&?1=8@Z5b1諾|UU-7޼Gf-ߖshŢZBIѣGYZ=8e2FXMM}oUqtusUAd7U wbc̗-~ʑx0a[X'I/dw)$I[UQqNp~ąSFy9Kpz63sUV6ug>/޿z!TqaA@||)3g&RqrKܳGwoܩi9IGǟH3 T83yRLll`Ȑ(k9V*, &B1ׯ;V}w>_q$I5pYow^uV?Ɲc M?G`,V 7${Nzq} 85OKX HA7;֠JR,1A ʋJ7xޑi=p]RՉN'Pf330aBy((hk7onݿ!Oy":PF#Ųtxnp݈aT"8/wgܑ_ڙۏ~|]GCIYq}dX'LRnxeכc4;gs7'Ӏ5l-vpqn?PrϐL{m{c,򕕭>j*|j:"b(sֽ3/OP.9p-۶dŖ+4QQSWWwRTyǹkqGGK5o.HkJHd$WܾHPKDC${h6LqY#/AN6lݻ駛EᅬYƼ`QTGU/_xwxU+kEK7Tx}_&I'X|ɟ}1Z}}5'ˆJ5OxUk_!PO5,(tgI@k|rJh4xƯXr oe^o=pZX#y$4 R_R@+yƹNu. MA=0"Zo6UUr/&u:sFSFqBSwhˎ |l͛;sb GNYMTO nHhصqݺO?NURyǫ־uZNt5 6Ȋ| tXzIֱuU$S&kn¯mAA͔.&))[2Ϙ,K&24{v}7ȱc0TzʤCNw9ff>XO}! IDAT=Rw:=.WK[ 7̕}y5(piq^(Y^N"&&orH83_ݛ_V0`HX6}"`?_;?u6ǺțY_eoL!&Ѱ[}4(z!r/"P=72''xr)DhWTOW}/pS3'15+,^ů[]G X}'pJ Uin~'ЧVt V?YZQ3_\EE D?,Zf"(grn9j{t.I&?:pw.,qޭ X>漛C}>"+Q<~T{64]ޓQp3iDi7`"_F92c֬ J9ݻO%ԼFl>h={k;OJ~F5)|ӡR)= 7o\Jth/uuk(øqawOjI&/#}O{. P|*([X;;w1>UW42~YmLzY]j*rą;mߛ>:ݧ}y - 3\goryLd IL$ǹoP;MLJJ764֮yܼhbqG_PܻW1;i0F-?D9HeWeّ|ySE+2֡@4=L.R{,((0˥;>cEsw |/,ded%NK˪- "p;0IF׆|/&w?53GKlTc?y;*4:9uWNtиW7of~Bќž tHIce |DGEZ2myW#Qtޭ,5q}V|a۴!+[H}6&Tqw Qtyk 1mF^Q՗b6*aj;:rDhV44ݧD]gsc\kFuhǪ:d.#J4ڨM8 8OV SAӀ)0A< Ç,G>WcA!ag0;UɺxǛ=E7-\f;uW`a0[ 6L ERvJ4'W6ddȯтC\_U°kCx;>\y~eŭ;X5谏 &bP9XlFCdQTg0m\m5ȑ`ٷOtwO2d]k/"qBo>$㾊kR{K`2Y>DK0;J2LÅ"O |p6[[I.,*  gA`_cG`+cQb 1 wY_݅G 俻9PQd&*!FUbRRP`ߡtz?O?M|dy^,_=}2]PL Ҹ?6_ܗ j"OS'r=r*mALWBp v{bu:}_a*. aKT:UjL :G)S=;Zh^SU p|07P[ w&N%eryaxUjPxl',ﴴxnaHk(Q /ӄk>utyӦlWrW W2 +4~)تBw[.?E|[bDt:}F_Rȟ͓łh$I:;U tÞ]YeEhMR Sڇ$*Yz`jQPkV-EyIIAAA~5 81(r>;gill=&uޔ)F  ˲zCh4c}ݛP:|؟Zy**.xABҦ6T /ƖtԵ$#y#x[h7A˖e5>2 [vx;(,n޶٪qǗD=s={ܥ~V)kf.ڜTxG^ :xroi1)S*yFA^:UsGSz b}έ&687'ayOÞ P\Q32ؐe_I:Jz!:421J8lC,GAAO?=矍S ޙ6M w)YXO֛C<7}cDMt#ڸO .REn߫/_)ĜADuQb~}ID>Xڞ=!+WʶBi-(P3T:A1HcݾëGG:]:ۑ="܄f!}qR#5T Lȫ~WJyɐ8c/堚soQ5f %َ Mz=Տk20~|7Ӹ#/g"w}z&2Rwʊy嵼ޯ9?7ZgxE@ib#wF<6jc.oNw'fHjg!C;ae (Ä й{4/\r!2/X~u_z쉟g2dwy,_Z,: 0% AyI[GC4Śh1dӹSQ|At$Gh)S5m)DWO}/L۳Gwƌ{y*[zhsoqt!X$ۚסى?q9Ν=4͚Y[zk $y^|tnAAY6ԝ:w<*)y'(H($qn!Id%I={A]ZZܹj㨼3pt61TG{ M# ݧ;~NxbĨ]*NQ H4M@ c $I (I J$I2ANWڟtЩl=iq4* /fN?3J zc b]b3V̋ n!p.0eO$ J27Q$HQE+euY*IR{F*3kiRIK\tSGx0pzh@@ $IcDD$  $_|{MEXD2Hg;7h8e͖ osDmC(! βS}r ]6쮻6my*G|LB>guU !E%##(VLxpkY4AD@"m۬wΌ2IaR$IpA;^1*Hఇq[U,@X`A>X:Z\~kg8rs]^}ꡇL3g\rItOXuo%4+[nQ;^8~(bX !8H B"[5fǢS+`0Uꑰx,`1HEEP` D! o,ؽ^_X8&y 82BXF4A^d<! H K=Ŋ38DbN:o\LAF+..ØXP?J9!5 DqnaӬYY@x]sǎyN櫪w_LD^GD!FV.0pm.3F"E atzhZ-"(VPV`b Z!@:تmQ cщ$h JF4ᖢNKaqwA͆  @kS4ej׏w Hd  lX$9XxHg1nBX -BH&,-0:-MެgS*r\sÁ-tԢNNGhv6DEQҀ $,5r] ܀cUQqANDy)86$Ai$ĈA6C#F3 La$R@Af͑u5WgdG;bh18΍y$q4 z0vcsn Np8jZyIxp6.#­.,"@"0ql,bhg+$u թ{դ!AD``Yc>j0.8u(2Ó4N=HFGEܷ/>)<0ƀ! FD! )DiiSgy@$ ,`D"( q.su}008 y(F .F\qz;"թ^2EcO&%aDHc,cٛӆ-w/<6<*)^vfY!1lǜxTp;r #$[pkn*ڗ_RVYRT4"mdL\tPjT\>Hd]օq=@:T\мg6 CH9ÖDHc0LzVڲㅇ"Cag!!5!;\!ZVK`4q<v`c]$8(/9_7[nZzW7Fa^ΌǵnF|RqYkm O!0h6((;Af,+A1=+9H5'Py _2l #z<lhjm ) %\6`H321LyQq)&5):.eartc cc=_QeBMw 1,t5Uaff VVWr6Y%1  c&1615%*.f8 )s`+Y=όnT*Ti#;U[ yE0P[daڂbp{ `,HOKHKuj撢b58>,e@E P1ޖDi9 acA,Ihq8R9,D0iwoq9,kh $DhD1!IDAT2J0ZmE{׵` /` weEQY*Tj'-}ۃRZS -GzBJX 2A56*/~z}Wj n&@gb1-DF',/D-Y@YslxF! ɘ2ktf#P۲ c?IHӑZ@ET *.x`7V,?Xt88hMV3..Z_h8b4QIm,!YcRxdk0ZUI7178â.XaL0 `ZMB坞0+9WmoƎ)85}yѩ B!kHJJ8]oo79@KPb;TT=H #zLpcmse ,kఛ#?"0NM⸓Gj 02ZX8f6!Dw- Qg@T3i40pWvB;=A3&gŬ3AC[}m$ #!y}H)K4 ĺn8 (3bIU 6f(;gt FE$[BZ^4V er3V$Q:90nۢ,VkX5ZX.dognؙ\ =#˲KNN6p*T#Bvɧvopnܖ`Y&{Xh7Z,̚-i&Ĺ812b/篿,.k7HK )S$D mzXBYV_\x1Z55p15.!*. lLҔ5YcnhjZւ`&44$999%%l6e*?;uM߭/ zfsD>:*FB pTTTjYdfMY3 eeUeIͬ paEOX 0 8 aqxڢyeEǎ-];<);6#rgɷ#QZ+Rd[md[EHR4A #򣗴ARȱAEР? v[PV,)mڢ,J\KqwJ,A6nھA$}_|ߛCg<6Y,-7ΏkRTi,y^߿%=R,H8<ϞIinm$ K '#f CԆYX (s۶:[?`);YtIJ؛ߙOk?9>z٨QB|xfΡQn,y ۪W!#rc΄Βԕ8ȐymDCP졙csDz/\/`Qc4ԇfjT(RH?sn}#)=[&fُ(RJ'^-/{iWwREq_~s"ҝDB;:7vtnG?|u{Օ;oz#~M^7 ƒsfQ.Bz}{<ב݁QHԞ#UVSq`9Yf\pr~sr!AD!q33E6;ёS'f*D?h8v7o(:ۖj#Jwa s}^ O~vxB)gz'zym3aM"h$%n }酗S荵ϞJVX`HD=q[Tn$y.o[یAR:WDGVuc}q٭nE6X@ADa\by^>]0IQ |^m`)< (J;s$M}ys^]jQaB `4Qqw ^kШo;;DGtmRX5KJZ4q敗W; BMiQBLt1Љgu ITY\ިvlP*:h*6R)+nX:z<O?:*u@Ͷff٬9;m.@MfmtOaVu}0O8Nd2tZ ; Mrޕ+Wܦ{_c̣<:SsqS.D}zfzu7*-Ј!B]wgoE#X\^Gp).41pַ`PLOMXbHN@y 1!E#i=zTQ(yxtF~R-speBR,K[C^ {!hz,%5Q7E"wb-Dk]Sc +',!qPq@D"Bƒ6:MU*ұkw|){뫕?@^'szؓ!'bX|獤5W(yiszz/̤fRH{l6-50HGSAWnG~<F2<5מyᄋQ;8ڥH#:c%&9;Sg'ӥpuiyqu%=)ُ43"`xMC4 O>966Jw":O8199y7nH){Q-?:I 'w8B׃`VTJL<={ OF"D33]4]zᕗV++2 ž)u!@1f2mͳfP(yxDbbbW_ ޓRa?a6Ο?>m^IPH`D'M>ό+()mJ<8Pw i򴷶^oNJd*'O?Vިm.^jn\VX)IAzy[hADY,x9/E*Q(OO|^n=9~kn: k._l6@# YۧRZx82t ηI|oem#3vƿW{=iꆞ#ܶ83q "= EU Rf沾g2uVPt狋BP(͝9s}"jZ.]j6Rʀ[چzy^ J9fi! ;H]Ji FWrB4i6G򚦩xGt翘D"ai`Ç_|NaE`0ZWƑQ,$$.dmRJYV^RT8!3n!D6Lm 4]LSBe;rș3gb= ð|'}Q}?ڷXitODCxO{xPIs#i$ɩZ ҝ/qyhtttzz}?82Rnmm% ۶yaA`YVܤ" L}ڵk@=T,UN[\^XUBTmJwx`0OiT*(fYVPj`0}yghr{{͛ۦiV*]׋c=8Na%V(J %~`|?0&'''''"*wk AyOQ(w:|?p)U`Pyaal$dHB em&JlIENDB`tsung-1.8.0/docs/images/ldap-results.png0000644000201100017670000002505214377756736017724 0ustar nniclausdreamPNG  IHDReU IDATxyXWQV ֪"JqAAQѠVXuyKja]@Q ګ"O[" ՠA6EDT*`%qfL%kr2s9wfLzvtBH+`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!DhǏۢ!Ա40`V/Bu,RF!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!,SUU@գˀBB!B B!B B!B B!B B!B B!B B!B B!B B!B BeD222jr Pzh Bu,RF!V97$zkeJ=`vv)2B +ehBFChŻ(>|8`ޭ܂EPiRuhHU_|*kU3k-UYy99 edd4z޾-Ȱ$4ʹTWiiiׯS H3R,!!aڵGSetsCY!!!ޑyٖГ'O#G۹ٵxn8pƪ_Ç>b++%K;wv;bҤI+<{lʕ]޼ysС3رMQD<~xݺucƌ0aΝ;!onee_xyy~Jm5|ߟ嚛s8.뛞N􌌌ӧOt{5.իϝ;wsٿ]]oZTnORZ"v9a„1cƬYFPz㹡CCCg͚գG222DsssMMX,uuu+V_v ^~~***;f罹fٲe=}'^zO>6VK}7 H~rU3{zz^tL9sF__Y5%&&R30eQe7:44|ż?d;\N>,::zĈT:H_ʕ+_Nttte~!C޼y3zh?tPT:jԨ={ԩSKJJ -- 0)׽5y䲲2HHH G™3g*]Tc}}9hT_~eNN|駣G&fff2ѳgO'''>}E[KД֖@Qٳ3f/MmX,i <[ˢQ?I>Jrrr\cS.I.?޽{S)=X,[2AR^;zF)kbbB&H)EW^F'PLMK-EOtEQd[yUwhbx<ǣν)IxxxiӚs~*:$G'xoқsU3bRwmmǏE"QPPPssÇ'755%''㏯^rʠAm--M}ܰY\\L}El7Dٳ۷oRiSSF;_EqQҥR)000?Ǽy֭[RD":v옓D```uuuUUծ]4̈́I5466ZXXXZZĚ*r YYYW3eʔ|<~{ޢtmKp\2{ݻw}}f^2*ׯ__v-XYYQs ԁ!jOVJHHkhhD~~~$}ѢEZ.v(//$Uu8'iAoHFBCDDD@ :t(O˖I"26@JI9CRl Dc+++]]]++۷_~ի-Ρ!uv*;W|Aj򥟟+eH}9NF ̜97UD2),,trr"4-REEE|>ё $666Fnymyz׿bbyyyrr@ FFUs#Bi1'MD .8VT翣j΃[N$?~<,,Llnnn˖-bx֭rogFTzѢEWFFƩS d_=ʤ.\sΌC]~.\wX'''U#gƘ]۫W///OJBOO^zQȊJӦMׯ}4-mT57(mU1?x9t6&L5j>ڊUH$~ZZZ:thDJCCC n:{l6p|}}kkke!3f|'3gdR~E!!!\.f: RQQ5yd6mkkQ^^.vڢƴМ9sNU:,^x͚5ΝnY>QҘ5k5'۲ˎdeMRHI4ٸ61-w lPhhhX]%%%ݻbˉNVvU͍Ub|||ҚvRgCEC2 Jg;|%K =kMш99r#ᥪJvq2$+ş9R)d2?smQcZ߿>((p8={y_n+W477Ϙ1C.]eFֱ455uqq  <9hQrK$ϖF򱦦J@$}gH -mT57uqƬgϞy{{99% "vРA7o&j!J.\8{۷;J#""Ba~~~}}.]J @ϟ߿?11lÆ y46mn;uͷ։ ӳ' $qȑ";0mki2B;fROտ_QW5!l^ˋP5-/B"FC" sC"0":aͷ?!!!ѐ~׭MCEA]R~~~LLLLLLzzzss]B?7oަMhϚ\WWgjja6M_TooV$6 '" >kٞ\]]͛'Ν۷o׮]:uԤM6eddWiHFm>3xRFHֵk^nLxbrr+\. mʕɗ.]R:PPD"ٱcۯ_NT=V6{ʕ cbb\.Yr%LVii?/8889rDv|CWZZb r5BՂrW|eA 4jjj̙39θq\]]^a=|B'''j\$666F,;˗/+>>>88833s޽NNN%%%j~>>J!$]۷o?xf-4nnn˖-bx֭gϞuW\\p¸;wfdd:tHv 0Ө}Ri\盘N2E[SSӫWN8b 43MEYSL111111RSji Fi&,~hlݺul66cƌO>d̙CBB***&Ofmmm=<<˙?eǜׯ j|*Ls S-Ʀ_~G|!!!\.f:t޽o|4j_rai!9F PRixx-,,NFGz\&d4Z:96l<}&ЈYfQ)d۷oΦ4CDJ䙔6͍b555(-X+++U#[XXŋ׬Ys9jk#B?FRoIIɽ{zi!+: ;'krrrrrr@٩/_dvf&ý7440lȑ#F =!ﯸl"9*R)]QQ6GiRHtE666...[lZ ^daBBbo>R",REm1o+W477Ϙ1(UUU| E/Lvtt4޽{o޼1u/O8~6jxAY@cΜ9 bdsNz2mgg9k ]Ԍ3j* ӧgddPXX[B<Ǥ}ɉY&;.kׂJJJ&M$lmmoo޼@<]du-w~Gͤ +G6^}}}DDP(ϯ<$$$))Ύ㙘ZD8Wn^7p@GGG777ٳ9 ӳ&;wܰ0.Ѱ) H-MG)**o5bͷNsP&w2B˨kѐyY%@;.PULђX%@;~ChBDwy!!B`4D .!0"!v4{}Fv2&OU QPg4^X,2E,=z4%%E"3fϏIOOonnfXjjwtt4Eߚ!!!uuuNNN6l` ϟ?ṣG97|_oΜ9C^]oiiٕ _~~>>P qe˖}ϟ%CDZ__/g>[3++kѢE'矮TS۷?ÇΝӧ߿?ٳg!!!0A}}}{STZXX٫W/___ &$$thH|\իW'Nb DF&)S|TJ)4Zp„ FߴiȌF 4BBB***&Ofmmm=<<˃eg+..^rT*=q(]kQC>&ݠ O._؄j~:!!\)D`ccӯ_?]]]KKKr -((غuٳl6%RYYrlСCZԶ&Ν;"d3gɓ'>|HF9sfvvɓ'T]]]!9P1dtT* ?ǎ /^˄ZL0?hAذaS&@#11rc.))Iv+W՝8ql*))w^޽\l:9Yյھ}{SSkbccTO!/^f͹s窫I:Tf(i;rCZGn[tsscX>>>iiiMMMiii$MJJBavvP(444tqq᧟~*++.Ѱf@ ׮]!m(-KkhAjf]]]hhh` 4АJ!犊 يoTGrJss3:HgbGRFFFk~Ç/Y0!!Ύ^r2r#F󚪪*x "l5mll"##MMM]\\,--l&3q8gϞ7o %JbK4$L#5A;۶m355eXnP-޿ko7ٱSx;={\~͛7[Pv2͕/S>b1IRRR._<|D rrrrssCByyyH ֜4iRTTTffN>MlllȷO<#FPB*۷qqq\.DCry=Q/͛7 Sԭ@9dbܸq eo 2ƹl<6,,G ߿'{OIP(>}̪޽tTQrM=5tttVZE>2\*xݻI_pwׯgX{-)))**ud<|p}}}JJ yIylx߾},k o 7`@H(y<ܜSNHN g͚էO)Sv$yxxP/`III.** 6olnn^ZZ*JʄB644+aҚ7njjjz왷w\\ȑ#o+''G*>zãG?3!00ԩS?IJ7zڵkAAA%%%&MԷUUU7oupp .Ku떇ݻw?#7AՂr%T|&{yHHHRRRee311Q7oxַo_zxܹaaaJX߼ysK.O}Çϟ۷z7$IENDB`tsung-1.8.0/docs/images/ldap-hierarchy.png0000644000201100017670000013363314377756736020206 0ustar nniclausdreamPNG  IHDRsī IDATxw\Vzo" Q,g콀grSφz (ͮtv-3?e,EG3yKfd,?Q0 iI"P?z7+ U)++%bc^|1''U;iJX/N!(oTWthD"u&9jt%I͛]ݺժ N {EZ^\ҢkoDc2226mҤi3sqvU57{-[\IUxe,QI-Sѣ/KOO7334h4(m[buy, gϽzrRRvԔ'kkkjл= <\NNgzz]t)331>}Qdzn<ʪ4cJl??/baCAwةFۻwwMM>[0 #Ir=zzy%]x.>fffCwuB>o?455ono_J2/22 zyݲhђׯEGGa޼y)SiLQTb|2xF|gL[rՙ珟8ǽKN?*`"]SK,+99 LMaG9sǢB_VXF̊rvn|8vj"/\8Ot$/^@IO<),,6}]3inn1g|eTW 2L(f47`2YHe,,阩PVՍ#JJVSm5bU[HKKhPWנN599ׯ]CNNAQR MQQRy-,bc/[fMlm~KWWO0ǥc uJ$ItkP?55u۷0Rd2GwE d21jR˓_bUǔׯ]~0೧A`eea9RmHe| -Wܤ x"]IMM1OCCS< N Bi4jԨc$Cn+Pb =ڪD bcc H$iS':wvui%Ig$))L^ ܄O8IZtz&q||1)ès-p8 \o|YOOO>/,%.((峌m w$33?}553Սn/ML茊qqYH-..yFq*8066F^dSQئE$33333Ħcl( %}]::]moX%Q`ӄl3]Z]Ri)L^S33iԜ:u>jbӄ*Hm/,%nAA2Җ0q2aaѰwow$&&]2j_& ?odae$&2+nRWxaPXX i||<}eQ#ټe^te(Q(Yb=OfPw bX, j[X@xx(ggg޼q]ZJD"D"znn7™'&[XPR i+p' bQ@?tu&- ǝ`=f >;~t{_C##$ɕ+MOˆ=1A&^]]Mq&'} -Z)S ={a zIUw>L&Ν)))L&ߺy)nRW$D;a%~P< EWnggDϦ {􌊊Mz߃566ջS'h)Evw(##CGG?Gn^ݟ> vJ~~s:Ul _=MM-JK֍۷m۶eTTnsԘ ZW޽ѳT߾}аi*3Lдl0etnss)S5MΙ3S'󍌌{]&Up˼7V-Ber iCj1F"\-\ȪQ#3Yں15=hj@a*B TDj1G߯^|̙|m,1hUSZIrU7^2b?\U~ `iYeB #jc˗6mbūB_,bȐa zs8_0M_OOJgȤ̣ڼ=L;eoX%Lmgҡ? >)d2\pGUfB9}P(V-\8ѹo QNCYbdQa$  gۆeQ *?5BuJR?C233 H&E׎6tԹrU}B4ggǸ>ۡtdA^6WFB<ϴ#qbX8 P q )a=qL) DgU__mё_ Eotw޸qϟMLLz/3fڵ#88T{ZCCqy..m*e2QR;vt '7 =+ bޤx` `őKgs$P$)Pà9"zEF}~kK;zdt*5s=o&7pw;`7lp鬬sgL<Ҝ^$}u} ܱBMFЫ@DQ(.fPhi BHaa MMB\ I4qˎ @AA!Kg͊2UXh֓?QKKϦO$I.~tuuqq|F__ ugׯ?+44dΜYOG=h$;ul7n\>|>>#Ck {MgK;%iEFKg5l'6'嫵Mqwg`L"KìɦD"WS;Pkc&\p{zRiH 0N8SqA=755vvOY3@oW#Hbcc֮]f={O;uLԩS" akAA! #Y_[aL+Ar[,J\sFֽ LbND,[B5HQ:'6}&?~tӦ- 0A1 􈪴c->CN4$a)trry]NxGgq-۶u#I&] ?; Xb Hn{wN50TGN><;FlmL$\L r׶S*IVnSOfRdjڴgA,K"wСx[tI^MÇ|?~}eǏڵ+UdBl֬gAݻϞۗ)NVK' lᄺ KXaaꍸ-撚-\\ssyR844ƾ/02dc,kQ{aWWFPUk֟#G;vb;AFJ.ZU+׭ K޼yu`gSi<<lذnE8 I`0Gݰa*6wEDll̿kWqKEN6}"Zr_{ee(p&X)@jj|pL'"SI1S$V,sl1^201&q1 d"U:Ƚ^UZKNv6.auuM751?tA8;xz{/Jֵېp###*&55SǶae 8trZGۛoaa91"SC]KHz6q1`I~/D-^A\6%䀄V@9<#\8za/e(xFS5ut a", 6@J  gX7,<\"YCLl&Rw+V- IRSp B[ ɬl2O \ha:ژ:f .`8$e@  fI~Q^$r L hoƒ7 ;#H p 88K&'M\`fu%$d@ {-2:2,٘|k  E!HO#]qLnG_j2E zE]*mfEPNP_S1hʕ|c~s@ˆFڒf>_1=3Ç"B w)@+nvCl&!%YZDHLjv6ðZҚ]^pA<4Ux_{Kldߠ}XSTϣVWM. R{\,蚚@߉Yp6@054jچ$I @ *yŚF#IB0̙T6~#H$"u瑚F{0{E Us5mC "PgP+Pݽ+Pݽ8s@_O 0L cΝ;`fq}̬sgq'k׾3xeS U5k9WĝvߘX, ;wX$^diۦB/NJyfp(/8B. (^KJ-`0;0/nTə3>,klcfҤ)ʬ+F&f.@7)٥Pa}K.޳a>ǎmİK'K=~SV&m8;v@_GT5Y@ H'ͼA?~O/ sSK qƤ/${[fF[:u:J#i˻603nde1aXi;Kk/_>>Qn]^R)C@_رm88;=sm@w @#gZm׭MM$I;wl`l\۶ntʕ˻w<ظD_Cw5!I[g6Q>YE'ޱ}n {ڣ5kYȘаw -&uӦߵϟkB11N۵s#I2=#3];ouּlqc1 [lvJVE/v2޽Pl\Ûܺu\f EÎjnn#F0LL3gtO0 wjv{*{Hx֟=L^jd bݻgɒ)NNt=ܖ.]֣ԩ}M?޽z,{[7Jaa)zdm߮ͻO^4k֌egg7m( ;1[&}ɓ=lffgׯ?244dΜYOGc u> ttq,M_,F(P+ݺy;w(&p;t1"g6qt4Rwݽ_PPŋS&O2.ZU+_zUXXq?GP"33AAAҪc\A1ѣGyyy6*Wʼ j'mo߼/_2`8&լϲƠA\WWVƅaay/<덽ұ%7sƴx s8&&4ޔ8}5O$K2k6lӧ hSAE>0@Qn'ԥK1GǛ IDAT31  I_]kK2% 4|iZKu`˖-.]ٴi)1bϞ=}vҤIOT2ƍ̥K5nja5i$) ٻƈ "== ]@AԴ r=C|Ycp 7XL͘T"sڽ-t4wvn+/e| ~/* #pa~sn^[{՚8w(2;V3k:v1bDζmhytNK  CCxA=0M48p,&=({}uNN~vL榣S FV 6T6%[fVƺ}jeI2Y|qTO"]M.nc̲T*Xi=IfT{1IPWjYWٻW H$DNq1do~ׯX,hf|Μ9t_,^f/=#EƍmgKԴQ Y֭[zĉ߼yty H2*im+[+=CZƧPWj,0- /KXL΂3挾;YTtRɕe?85$qwP9`2fzTz^:1>}E/_dKs/\Ƚp۵j=zPC6ff |6o|ҥK|6ois?۷o?xR1[>~iSVV6++f+{!(e cPG~N,KJϓ ,\*I`1Yl k&#nqlv{_s4h?e*Sa8<(IKՏі 33# * \Qii %m664Хum{^~^1g'}=Aۧ_>T8,,L_O٩Q ..nҤ v6FzvvMN3Zϋ вQn]_JJ 9x𠷷7͖3g-pB׮]MMM6l8nܸT:͑#G;&4ѧO ְaC]^|ݾ}K.&&&'N(`===Zӧi]Y===\tѣmGEV@e.Ӈwjb![ő4oĨAsk}/O' $,XuoI$J$*';KzH$n'DF\q ǰIN?tA=urܮsI.lqL Y3ekK5 2ضh~}ŋc! M&''-]zq?zS~  `0ކEXa[ /_| /^<ĉ]m΄ǏzqwJdG?H˯ߢ[r8qsgXU?Ͼ~} vwڵaÆׯ_kii͛7?~Νݷo߶m.]Thܸq={ ٲe˵kרx// Ĝ:uj׮]o.ͤݻwn޼֭[H& \re݇޽[>Z+H,!y8lÖDVN.Wlgme2f1f^$;O'-455W^ [nV`UePrrrmۖb^ѣGŅbieb=Jo۶mǎ}eXذaCi&899X,gggݻwG\T`tv'''\ j _|ɷ$.W +<ȹ,3 4; $;>U'K%^|<4:E]ܫZokOU#k[qG9 GD)c,=pNSs mEڥb!I$A@"岹Sx])Ο01ODgW:MfVvak׮>qتU+N9G'k&>,, [N@hh~]te2'wNFHwСÂ_`2]tRj*$${o55&Mvs@H[ܩt1ҧ2VUoõJK`llLk0,??:S˖-iQ˖-?|PhԨQݺuӧcǎMMM4aaa&L.,E޽O2͛͛Z+4%HM00FjL+"@H@j6\T_7[M. iyLfL.XUO~'3La0"RL;ж pS\㼀o}qOc1 bOq-B5L%#xeH22*P/kO?qt|gkkk;;|?f8$O<mvE{;5)/~@' '+nիW{VZ!aaeD˗/4hгgׯ_?u3fAJHAJ]fUAY,Qvf&I@Pin)c` kثn49NE5kBBBBh|@eVx<ϠTeAQZZ\ɘҾ ??6"kח# d.䀢# uKN9|+8ur ՃnhE͛ʼngϖ'33sժзo?-Z?V^ qot{ɓ40wvqqqicfSb[GK[)Ѳ+l{!Ξ=ާ * Vnnnݺu>|Çsss%IDD͛鞝4tĉW\&HV\9y2E&L O>͛~H$={6iҤ +WʧM&c볲$۷o̙CNbŊ+WL@֊5b3150%O-:G;@3%KL iȤ $ 2nVQ? wAG:@ZH?5S޷$IhGBe3>IVެ za28U^_<}Z:G-aff6ceW*H߭{l޼888\pY[B}A<<<qvv-X}ysggee :E3ڵkm[71ЃuyUU^w2oŋmرc ,ٳg^[ ZPP0gΜDsss//AQ &Nw޷o߲m̙)))'O^f ]|@f Z #B/406`Z>I-2!GYY8 40]"i  %(~ 2H|P(6Ɔe(ߚ?y0 &?g+ؔVyt]ڟ`0cc6nڼqfd;vZ''Δ˪ L{Kr||cǎ;DzѣGxСCJ)WZDw3f̘1cpDDDtvŵV 3Lvws[Yml8[Gy"Cb dJL`2cڵ':OsJM@A h_. KJ|+q> B Dݮ5={y,--?dɒ#GxkK̀L$KO-*#0+amR0 H Xe|B'6L-@@)+sAQ1͌XR$Xv>#wM-1  4TFcƌIHH0559r3jCRܜcZ#=l܀1HL}K73g?3,dž5ͮU7^D?5t QxJō򅌈pWG>-G1Ƚ"AwͪJj9rȑ#Ę*!t2⒲ZYkYLJ*N.0s,NƦ ⸤:¾NLMM-U֗$  x@@Bh0] @jDӡԑa>GVM̿tOo/,'~7 ~-^mUe^y#J^)K#,C \:\kpq .58X6_Kp|]C-55"  Wt {C2 @-EZ7b$H=*[^G[/{0ˆ;'9!Qى&LC=u !IʏJg5:Zjijj)s}gϙ_%_W,ACBHLtm4ssK?k H 2䩼Z$A]Sr@A,` A!#*ID#(kqk39~ z=>@QaY-ZZح`]]q|x :jCӁp~m_ȩ ܂|Q_٬[ʱJ$ H&$N$HL\;WRrpܻwWMR_ػwo+G _afcؙ3Ss1b2LLWip5556]1[S/ag-?<;߻D t}(K=x/ !<<4U۠+BYZ<23W%Dq{ pu"02fm͐{] q۠acnkhW^I4Zi_tѭUCǦÏ}=mNܳg7 sSC](>p]:::8uÆX54751zj!Q$A 2Ғ#<;wpokڨתg|6%y5ol-QxoOȖV9{ظ3gܵQcFޫoݺڵX9lnL_ރ +#u9Y:*YD啨P48PAnN#Gmmp[h@Ͼ}nݼk=E7/}@\\\SUQ!*aٍm[,X 'ee^?;o{=3D$L,vom '2eЧbZEM_UDGvxẹ|HgR{0 I6ú 88:2nh/zԘ4y[WW.mtڵAr؍@T/Fxr|rr2l{zHp }uұLwx qV]Csʼ5h?' 7L/QaJUHP%:_`0%D&dfуu6\x;ر F TCCq%&x l;~ڋCۢuђ1NVgsXtyNݤgm|k}N{w޻5`C-qbT0 #Ie}{֭CCCJ:::5{Α#>쳩 D T@YyC*e~75jbൊ6sVifF*x!<==sX4eZ^^mi_of (艒vҿWXG#DÆ~a~~~^^^ࣇVVTh4Q9b}& a=>*Ƚ< IDATg~졊gjnl6:% 3EQbUt(" (^jTg*!*m=“)mFeX̵RU̘>@W]Yߵkl[5޺e)S)Ѵ3nlʾ;yte+@S]zIXq^rQ[π -~/1/AH$b>ELX  Hy$G 0hT QiKP3*\+W^t#Lbԩ'ܸ^uظs/4iREB Pe3=N+1 /D"fwy9tԗǹ8_>/ߎdS'oTLˋfx8Hp%~P3E>"y.X@`z QHϦ=CN?Iث'2Np3^>c~Tԅ/d=ypɃ4y0lY ]zRLq08  4k{qQx D|2> Q`fs L[t4s(-#z1ԢbV6\v/`ߛ]:vWS6zrԵu9uڂ)y߿o9+v(YL8   @ (L=S]BE%r`3ifE $\.YT$kbEAz W} Dhs+OҧQ-ߪd)-2 nr\Qª<@ ]TcH$DnQkȩ۲XO}нp+ vt.(`!rJTX|H j@JdEѩG>jSq& 3ZIjz23OTX^ȒҖX BefEhp.}"Ҷm٣3rh~ceJ{`Se*TPEe~KeDu 6tђ.J`Τ6V|Ík"D]WP Ƚ"J@VfսoE ~ {ET1"½֞DA)S0׷iӦl6رcҔOOOuuu///NӤI6ݤICC|Q6ݴiS___iэ78NF?:Ǐ}p8...ϟ ?~ݿ?u~Pԋk-yXݼyӧsssϜ9vګWnҮ]̷ox%K(ɓ7n .Ȩ 9s&e,,D c<>ubz7 xP 0CN- J8P~߾}m۶eڵۻw-JHH?~<577߷oߥKEs}oߞfwa߾};v}E֭ pN>rJJ3h mmm6ݶm˗/KcE_M+P "uRtW"r%7 -w  Y}! `u}TX޲i.29RSS3 77$IPH<A|>_:DR>ڶmܹcmm}mGGǍѪU>|KƎ{9:R~qYAR^+YJ:r Jw+i,  pJX{T!J[^|).\H;88lذ!33S ܼys䢙3gWpp0fܒ,X ӓ^z…ϟ?/,,ׯh1wwY[[Kѣǃ͛GC077t( "uV"p9`@:jY`Qbcc---/_>d*ܹs>>>$I6klƍ\4cǎ=ztllҥKGUC1bDbbŋg(z%٣G-[,[˗Ak׎vs׺u^xp\]]\"ɓ'}^x ƎwzPBeV`cUidbb\+,`>4fWo.д %/^z?R7ރί(98tp_+KSDYj1}'&>ZZՈUu zvi  jPL(Ĝ/+M@@ȪP⫭z.,_^B% @ *WP Ƚ"J@@ TR^*12Jw ՃoЦ>zz Q @ * @jDMx6mp\+++=vCsy5ud'dA- Eq n-(uu*UZ |@GAu!NT, "8쐝ū=sW QkT?+J^nUΜ9coo=Tz]$% b￿G"̞=m=gffٝ9sv%!!A,}ouB;?tPnݐ wܱ=rH}ʕ+&&&gΜH$?ѣǹsӮ]x~Wiii* WznOEVmWZnݺ{ӧ]\\D"rt^bccx|ݵ3L&H'&&𪫕ch{{GAR)oŋ^^^$`L0!??j:))K.H Z[_&Z_ KkkgxնomA H+4 0vh#p$ @  , @ ^Y@1Ronm/h i4L*@4UWmTRPf"LLR,?Œjl<@2,n 0 TB!N'MF+Օt)/:ZxJ%-w!SHDN%01R1|K9Ϧ3pC#A@/0T_*c 03K PcTo*^I$R&/'˖:W+J=i3|f?i87rs2J$=JrqA66,T*'{Ӧ rūWxI ˗%q|75bxr*J( #&F,WU"BxdIT|PO%+a5RB}ZVRITU $''d&9uԊGK|2F3f̓'O.__STz.4H `LFPYXV'Obd\yݦ[0~h$b^hK]tnG2ᯝc:;U\3ܬ,1lӞS/ՈqSGL#dq@Q\LR"4p 5EEQFYX,dRFP!&c̐T*y pCd0nxFCtECR"4ghyhFEY`XHP aT,y4ݣؓ! 9!!/Ӈv߯څ fvi_9soi\ ='R1c0F$/<0IN^w]A?>y%\w:čXK'58j.\ JHHo^~adfB8p )JJJpԩ#G 01cFqq1b2d\Ej=\)/RQjJgʁfj}$?}~{jE/Ӽݿx%Ǿg_?̦{i`onK9ۚ ER ǙY^*Csf%ј4i\._|y^^^Ν׬9vX```TTZvrr駟ԩSy<޴i޽{gkkozҞ1y"!4.yjN $t<ͮ,O.|%r߻a5 ieW\Y?ߺ:VNJRjˮ: ynVP2Ȧ{O WXXwi/Ŷ|||RiqqšSN]&%ɣG̙ e2YAA?j2dܚ-xZ>OոýUFnֽ^+ܿ5(tiN%L='0Z",{t KX@ M څj)s_j_8:ak_w0)Ɍ[`j~1A3:}mì]{_ZMp @#_y_-)P(oK 6:ks~N-+fM =V*e__>ȷjf"mM*7/CmM-%В^ѽXRPȹWUsh~J/lY9_7s\vD^k5W4,Zu&)gϥ1{xlҠ7_ȢWa=fo}~{vY|AWlF]mlb>VEjg ;a@Q)vgU/nPշRX.KŁ/+i bX{~+ķ/Mgᵽz_H "U}|YPv hZ fY.g,d]@ D'D"$JI}A HsS^S.IeLs1S"a0i4P$VzLz @^y1e5ՓC\\HtppطoZް%Ϟ=?~<$HnnnǏGXHnݺ_@#Ip8N /F\@^>tr駟Y"8J$a$%%EDD>| ONN6r[?~7oAll'WΝ;wڵ|>bEEE?m}vi46*+Aja2z$ +رcϞ= "޳gOttڨucHoooA$=<P-Z:eOOQF}WHÇ}||kjs &B IDATZDr5Y6[LS^~юM5k-?RSS/]믿XVTFL:@ӥKwz 3fU[gluvvtp}5JFFFj<{L!Q[lO>Hdtss{𡗗Wi|mC"`pppPPн{r{.]Ǝ\~Ν3g==7DB0%%{HCBB޽+JE"QjjE]@ y]㧮-saE5qF]l>P?=<_>x<>>>-/++2e N'...Gzwj/zyyH$1a„|GAR)oz:47b5"|Ѽy.\ЮH$̙*ҥK fј[[[ @B[޽ENNv=z,""͛ Eի?~ԮӫW/R+/_^hݿQ 1\՚2p$яg]GN81cƌw޽~έvOW\ b@cC48@z8///Я_:k7A1 Ǐصkq8_5r&[4˖-b6m*//͛X,z V~͛?|P^^qFmkIIIٳgk[p![__4p333@AAA=yX5E9">XleɓV'Na5g$(3g,fc5̘KXk}ks'@ m//Ç+{zzJҤ$=z׮]%%%nnnSN~llرcz0rT*9sf- bŊÇG1kK ?%%[[ec$o#&x˗X>{6䄇#M֬;'T__\͛f۷KׯB,NhvX EG˲q;\)8tHz:y2y0#qz43S-ּ& qC / &hС7nh[޽/R)/*Q@TxDgFYnZ&SD£GI.U xIV*ǓܽVu4ab}\xO4 1X!!YGNZtD6og:06kjXd[[[T ruu%L&sԩtiE /*:{x*SLVXvQQ=,,;Q!Yt3f۷cMMǏEg,:z"!hFÒ%gUN-AC"ŋ"""x<^jjjFFjܵk/,,d0 .E /Ym2P`Ȑ!ĉk6C_p8666B^k# % 1aÆ-[[s%++kk6 1?)]:4CϣGD^j[LoM"| b;4 yos˗/wҥK@.턤ptB%dhF "]ǚ"]+V8~xVV:"U_g V^;([CCC-Z& MBstA _0vD \4iRttt1cj&j0qqq<ΎF\5-^8<<̬K.ϟ?yWeԨQ[n]zŦMH~niBD>>>6@mѹkN.A'Ǐ?~|jhI;v숉)..ܹ˧LV-ɜC)H͛7?y͛7wp8_9s<==&ѹjo߾˖-CSU!=yù4{fOHرcǏA.],Y߽{WP:;;W'6))駟~z7o.\,,,vѣ###~~~8Ν;x}:gIUgPP22xTo*k2jϣ M/BU1[#A|z*XQc 1՛\O/lM4"yuA }G 4 33lllvڕ 5@6U M;^(HΏ_ȁx*A؏89\ZVT£B I!^%QU@T<䮄xH& jZJS'Qi6P]pzx~#ׯS(޽;y?iӦ]rǏd2FM Mltt!ϗ~!~ZU(k@T-rcXF™U4m.ϗ+RP!{&xHsõfNիΝ;رcرHw? 33!CZJOWK.믿V\rMMMW^ Slbͦ;`(ǽut`b(U2%P cuJ㡠2DY}9tqxh qo$[ҫͻwj<+[LlejDmR^gcWy!sƍ4>_܂0X a0nxFCt(VP}X*CĐ -((8 ,KAQ)4 !L%"@.^^^l6 'N8ui1nT"!`0/y;wj 28>>JUҬ,mV, 8: F9;Y*c9NYJnHl2 /弝<SFt,d+YRN8G }H .n/UVm޼ȑ#k׮mm3%(86h) eO4k"a~)bqfyR]%2%gVLDC;]_!(,kÆ O}V\9m4ԙuui͚53?;vxX,V*[lH$ŵA |)vm۶ٳg7[@ <IxxxllѣBMǎC^:ضm'F*))f0Ӛ Z[;rȂ {ĚKk+99ŋwiWHhZs׭[]L&۷f͚5b Iھ}{dddzzm}oi`xjm!ׯ_ڵkS ԗk{ hܴ] .>4vk˚e w1mTk l-/^nfsHdͲfkU@VL5˺|v9c!JJ :!D@$VzL 4-RlZZΝ;wİ۷7I6Bׯ_;v5*66T^K' @&gb䦤bnŇpN$JI}A &K.6r}ɓ'9::Μ9|ӧٳ{ƍ￵_~9`n"׬Yӯ_?@ޙ3g bkk;bĈSNi\97o_= 0D&@!dJRJϪK@:M%_k;r#VdggkVNIIYxqΝ5k?TC :" L4I./_w#qz43S-W@ MA+^˗@h ^L Tx~,:yRr.\{m-Ƅnm kvSP ߭# @t\-T,!I2QYTe(bL&eEQb; !K 2լPFܼY>og:0B 6MUtU8sseYiQQ _iMH?K*liVz@1P #\ERA HP^))└JhaC&ٓ'2tՐsr1 KxltD`xgL?OHs@ .<ڃvA)]:"u v6=ƦG(?|e~ ]l(r 3/yBjgВVItc Rщw}@ۮVF#T9=s7k 6a_0[^{HskŐdc| c16hD6EoT ݖM#9bҳiq?~uerC_!u`O/ccAY3'O$J}诶,#T(nj~_ʮE%i`xagy\ȣ)p21'e]z#_I2%~R<-\ufO>mI~G4XA!6F;^O FWkuc?wfpk5)BL]\Sf ^k=1I>{o *UD!z] iy]S\e>e0EL^)*kZ$bN[R+/%Rgl{ IDAT}^~ aRF cWK#mk;a;!ڔ$:~Eyk5w *6Yּhm: kU)E'EODg"͏Fp 5x1<GDJ;Օj@%Tpf8,х3jYU)}(j DD+k2o=6x}Ա'|K16˯!+?DEOOI)7_7?-`0ՑǪīo'v9WTNbzҧ@^4UiuCBFr%r-%xKgәLN8G㡑 I ϗc/K%JұD7 $Ws)͓epq+lc@fLnke';>r݈x쫲Bg@Ln7sWlr:nmepVgԕ`4 g\.#Iљ&ocuDM 骹 ܱZ&=jTeFu@t$w"&Tw|} |K9@^$shx $ ,[89£B$KxIDW!$w%TaPz_ PR4_ V[|Ю7W(pvIZvrq美刜-9:\hR>.\&MxuiABoiWQ@URebi~4 goJ|H(U( g2noyoך!ʙC[Xr̡& -3lrt Ek8XQ`eJ<ꌕCAej%щs1?s  {C?->'w^vr!䠶Z SD{#W\8~RYʙCF+)l3W+Gd:>=\f׳_o#"ޖFyhlb:dߢC NtCIj&k35?P)~/x31G«@UTԗgwoTճIXokZ =;0x=->qp ǒͦ3t55bv~c?Oi  1l[w>sk~|`KAoủ3b$K  ѹXAbX CHb3<4,p L,eEQx(d0 vX*K*Ji=l {,j@_@'8ȟE=9Y޿.ܪ3º7cݸu.us譿S?¾g_@<7 w ߽~\8vy;L1k] Z\w@"k|#-Q)98dOd'29z@ŕ3I{WͪI z@uR_Mpitf{Nu;]QZ Qn iC F"QSDOJlDMxB.k/s/ 8JI^ԝW?wY~1@: +iR/ܡc'S>L}3zǹ7Rblj()|ݶBCسe%<67,HyOgWñruQ~Tj kp&kgJEחO'.y]A$4Wwl sbAύ7MfaaA"{禁43#%aXal7B0nOL=oö%~3j=smaos>f_Qa:=gٓ3k+6#ֹ];woH'|?s:Y[j7ԝy0T6-0ѣ7mZPP8rȵk׶_ D'|U.@Vv횧'@ݻ#GbbbZ):0Dþ} 3==<{lL&D"?yb 0D"um===i4ɖ-[&bq-gQ jj !)))""B0)))<<<99ِgo޼'N@ʯ^:wܵk|uyUxx9RI5m6{lC@Z^;rj-vرgϞAٳ'::ڐQԺ1yII7 58pۛD"ȑ#k C !;&99ŋ5MHYKVWzBӧO~>|xnnnh"WW)Sxzz5ꫯB>|]S!$''[.--N7}@jnA[*jȚeeת 6~kurBѕ@tB$bnItW[G;6ՙjQ?KMMtүbŊUVT*@ h, ¾}"""lll ?H^ DZu)MjF.Cd1o'cMc%Ij 윑w!322j2q8Ϟ}vېWWWd6Z\\ܧO$2=|˫tؾ}{LLLzzz׮] ia^{/@Y @G *$j: $SUoUzVF,WU"B2U!0888((޽{r޽{AAAK.ELcǎ \.s̙3뀞ƍ?D"Pҽ{w|!!!wޕJ"(55u„ Ixxxllal|ԝ_ [=I= 1udfϞ-f͚+[[P4I޽{\r֬Y\O/$HC=I>jԨ[^*3$$DW: T[p8iV Gi^25\$S' fSK;Ǐ_k+//5':B֞R=&_@=P.hLIis=$HҍC4L&@$Ah}A H#GDr52G@O+=&I!;vdvvv?K.'Op8wwp4Ht@ MãG&Lѣ}ǧhWٶmϗ/_/- iزeD"ٲe ݻ+DEE9::9sZVW4<|0h ]z\ Q`x@ -D^!@}vCHv{#_ 0B !44D">|PP .:&uWket([\xo߾Ϸ_`vvD 6gX$ɶ."J&LԩS+**@YYY``UPPPee!살.z{WӑCw޽{Nf͒ C"ŋ"""x<^jjjFFjܵk/,,d0 .^"]nnnl6 u_ B@^eiӧc͕ee'Z 4??Pe99*mTRPf"LLR,?Œ>|Ռ]U\ԩ˗r;ii HRg6n$ѣBMǎCXXXl۶M;7`}߾}\teϞ=={ܹsQP."ص`^B MU,)HyI ֖lZ*ܾ/_bi4@Nx8b2[ARPT<]vXbN&LL޿^㩓&!&1$" @GF BZ-T*vWvi׶ꐶHU_ڢ%%J P(5ɤpCwEEREaLDj&ȸx{{R'^^ʨQ'7ɭBQgluqqIOO#Ұnu֥Kd2놴`*H=kb(-e)ʀR4ZTʊط/}qJ ?.Z>YGԉ2ly0qzzn C`hhEׯ_oذA{Iׯߖ-[)JJJ!;w*?ŋMD?;wnmt+gO&Qu- Μղ*+FAXӋ2A«i:7yPcɓwk3asҤIr|yyy;w^fM{rر(ZO?٪^]f qIUxeS^RaOSyZ.'CMfQQWrr?btʈ"$-qΖܨ(EB`/_v4@LC&NϟRl4+ =l { ?.:}1}Xnőճgiv«8%Ey[JhaC&ٓ''Ot )a4cHv-Nc sWHƆ@ f j[@ H#Wi`x@ MÛ7oߵkW TK.]txɓgΜY\\ܺ770B ! … ׮]=ztddv(jX[p}kWB$(3g,fci4gf\Ag3Y6&yxmnuŁF25˚2boau<֜2m%:MZ:aef5I$(33V-L, \´8ha}ꘕzVͬN[Y0gԠ%hm%''h1c>>}h8nСΝÇl6^Aͮ]cccO>mmZ O_14)JJ>.]Z:eJ46; >> eڴiW\#LhMI-c2)VN}XDY[1HIZZիGb4__T@,kj,Wư>1#bAQ@LX:bEٴmmնg2d>qĚu4ccc{TSW7 ʣ:"";V*T|^5Vf)TGV :*h;;Rb+">rIB8'9#g>gǺn~ gٳgR?=zZBBB Cñ{D"ё#G#FGGoذ!**²>w߽͟ۦ3^'Θ2-_o޼:K.}g!:u"8o ۰j4)e_`]= C]bX5E譐KꕋkՁ)eK,[Yۯtt iŽ$@>[v%KZ*77W_"[ 0Ѯ1ڶ'%h.tKc+ت*m;TUP 5?ȟK$H۾Bs?CO&؆Qk9kxZS'Nwސ///ar[Ӓ7ov133s.r GJ(;L(Ⱥk !dt-gf(wߴof(݁W}s={m5%.\=v+W4|8##&uJ\V;w>#{PI@M}daaD"+^; \P'e2"!P:Imf/xX)Eh:Q4C%"&b{7{QɈb^Gﵵw^vEaGߏl6nwy뭷v޽bŊDa[GOJ(;kߟ$$v.]N "”3L |||b{z̪]pws1zvT*>}?G6I;`.4 +FW4bmצiښyyi7|3((fв5?ի9i[WhSD"Qǎ7oܥK֞KzڰЂZ{ #]@PxxmTTK.|m*x4,{Eړj}׶m}6@*k۷ooR A@8ѱ .k~*G+jlKף\t5pmlԩ=\hh sv=lذ~9GٵkΝ;7]nܹ#{%K_|l6uk֬6mpMaÆS- \r}#3s^{ѣeee+Vo~i ,pBNNΚ5k~{B?!$33>__eee8:|𨨨sFqoh{\\g t;w/oܸQYYosά/~g_u>[\`8qz1{PwyrY9Qt팍qØgTʍr<{m_ߓ9<<<''ǹZh]ׯ_1bL&իηgff~#F?|VVG}$ZjUtttǞ,{BǏP(/_w߹u!!!ݺu !^Z{Xoƍ/_%z׿{צvpA.{VNX#V'638 8(?HU^ G5 j>m۶Y Povll%Koݺ%̜9SJR7gO/%%% >||{ee…`@ &IDAT {ҩSݻWVVuSqqq׿! 9rd!w>|8ZiiQ n'aޫGd5P{ԿQç\BR*VT.[.f?( lٲk׾ """/o>h \ٳG|5kW./;;;##Cy//N:YF ,lݺ5$$|׳vq…N 7ϹvZqq {ァT*nܸqUMݻl6oڴ)!!i&pܹA53gpGgޒhhŽbbb_~띵q5ڏgʜ{###5ݵk^{ٳgBzYRR]wȑ[*CuSaaa6m:|ll,!$66>͍{C _;o{ؿv^6l[%jNq)'NTT{5l6aRJ^y啴*e;,tM2555&($$oOIIIOO?~f9xԩS=u=>3NgX3of+Wj4GΚ5|׃N2[koz#&8cʴ~xޫ[j=֧~pB__7xpI&[K.峕}N:k׎_C֯_T*Urrŋ[p>-+"$%%%%%5lwV```o?a !^@PxVi+xqf0h>@PRlOU';t߾m7+!Ք)q}vC}ƍFDp55֣G 7:FB}{^$qVzQo% qt;koCfy^\m)ǎխ\\"E`gZ~6z\RN5KlkeJKE2rxܹ !~} 99Q+C9f (O?<)ob{U_!B_MH:e޷z0g @K-T[ǔtzzӳ ̅ nw-[={ ]tۙ30yy^viΖcpTK9aq#ðWVoؠ52_Jݤ$9_|7vO+pi.Ka0cF2EѻߟDΚo鐕Ŕ.ŋuu_p ;^yE=th-V&h&VTTo&vH* +ː[瞓##}~\XX~=u}g~e_X2h]*vLq׮knG^xAjw߿+Ms$AAxet~h1cbbdaaӧMb^UC:)IN$ih:)^+"\NLgdx*  bX2t2n߮IIDTz_W+2RTtg4D]Ot`}۸k#uĀ"1LGs'.wtRi;&|v483d]9޴kU9w.7:j\Xh.,lc%~6-.ٳg<[[T ^*z @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@PxⱋW88k[sBi.[:3}q*iG{k=/ 7Pso[*iԡsvj]"᨝BWirFNhi.XC)eD2rR=W[j?_嬜=5(fFv&([g[&*)88a ]rjkl=s!v; [ r mv8a3*z).Ys!JlL)# Jc8*[Z9Jw.RzeByX#VMQ)z+$q6@$% ӘRFyR]5_[VVoۉs@fQ){AM Ty<}s },eX}){eV+"HP'sQv&E#  +^l=QRR HZktrzeԉꀜ""L9p62""yƴBϹ7̙vZ諠y= ]jA +|b} 3§˷RZ^^>m4QFVݻ7on׮q;sEz~ ڪ>Q@mr w&ީLtjጚ~U+M&mjcTw&Ѧi}||B)Ӊ'.\p„ *j)))νnjj_׮]ܹi&koJ8qEƍBWmv8a3*z)3lgm9b+1$P·+'([#Vثlj(%%jdz\parrS9qDrrsQg6u:| ]'N`ԩSgϮ(xFJkĪ)*Eo_Ұ86%e22pSHi}Pȑ#_~E8Xlو#yDRoÇGEFFR!@ʀg?J߶;IEA_5gp^ԧ/`]| //^|2˲rj6uXBd/ȴZa*7g$S>9L]S`Yb879sO>EEE[3j(̿|W~{Y'+X>(1!*bX$)Dav&E#  +^?JJJ'deeS*FEEeee ]O^hȑ#!QQQ2gϞYYY|ioxFJeԉꀜ""L9p62""yƴ3gNnnڵk*/g_wM0!++ƍ"ĉӧOo;w~wʕ+W= CJ߶kӴέ+Bl%СCo޼y-[/_9rΥz(xqXxl!X=Oh_+L[צI@q3ʀgz n8 <+WpϠ8n8^@(xp` @@q3((xW*T ^@Px5<ژ{hA(P!%NVbh CI;Vss'N/?=29NՒ%)Sdl%%lVK鐝_p%M3wnKE߾q\MQƍQcpAS+"viSA}{^$qVz8?.撒0 .(pNUWLs,4T-t;6VW- ӦUoU*ߩS5fi-#X+&yOl!7)-ds/wfsa>3S㣚0◚jٷϐ0ž=c ^չ)(nj^tLyyuBy~yd];B:>;pްegOrD$Gy d)*Fimgp6d2)z7Ç9աvl9vQ]M^o).JM}?ҐLxɔ ǜb9xg0_ʟ3ׅ^a0k3eݻ3˟{37.F2Eѻߟ}[bYP7R%xn6mіxvt%kjABWLiꗒBJM;b :Lz-ٳ{8Fydd4ubC'W3Y׮KYν̥K^]2k " e />~& !f^~y^c^nٳxwoQ'&JCCD" W'%_wu-[vN5~~^+"\Eh C%D,k4C.5s&?.fk)ԹIENDB`tsung-1.8.0/docs/images/connected.png0000644000201100017670000027642714377756736017265 0ustar nniclausdreamPNG  IHDRmsBIT|d pHYs&? IDATxwX?,,Db/XP@c=FELbLxsYn)4xSD*~5 X "T)*Jwŭe]߯هٝs|f.9#(B;"""""""""c"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec,XDDDDDDDDdј"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec,XDDDDDDDDdј"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec,XDDDDDDDDdј"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec,XDDDDDDDDdј"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec,XDDDDDDDDdј"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec,XDDDDDDDDdј"""""""""Y4&Ȣ1EDDDDDDDD ,""""""""hL`Ec1vyd2d2w8DDDDz1EDDp/_ ivvvAFн{w̘1[nEyyy}ػu-ZEa֭I ԸEDŋͮ'۷o_ODDDM^y1o}}%EǏF(cǎ; n޼ ȑ#z+**!Cp!&NN:AP̙3X~=N>X3qqq/Eؿ?0m4@R!==[JwTTxѧO4mArr2bccV9;FDDD;vLaoo/VTTI7nmժxi2'O[h!۸qNQS NNNCteff7Arxʕ۷VZI߿_u8{4B^)K1Ǐۖ(bjjhGr!iTxEo.ڊ O?*>c2w\[ÇK&Md :Ϟ=[j9*++CBBr~N,ͣuz?;ÇAJawaFDDD5,TVVݦMdug{ŊwȋSO=Scǎg&ꫯлwo۵k_PQQݻw?жV^ףz.caa!ë/((eJ۷oG.] ܣG<&͘>pttѺuk>*m&Z … uPTشik󃤽FӍСv%Y0vX,]TH.cݺuҚe_|~vgϞѣr6y<""XDDDJ{ƍIYYYHKKN,;tPt p `KFcm[6loÇ7zAE -m6ܹs0ydɫ8?/eȔ)Sc^6؆Kv,}ĉjQQQAirL8,ۮ];mOQTTTH"""zh@q1@rr2<<<6O˖-|ǬM[RR[n5y ʕ+8uT׵G59hu!11QUs7;C R>|`cc???BNN_cĉh֬t}CJDDcBr 6Lz?ֺMOO6Y̙3Ҷ;Y5.\Pڶm @=& WW]ުU+իWM&-_sEQ'!NNN5j.]$dggK25TV}xױi&\z[l `Ν?->""GXDDDl֬YvDDDO?k.5eAw0Hk:3b4li&?~mhO45UuSi=Z^jryyyذaCCС?:|󍴭su d5yd)lٲ2:Zh!m#""z1EDDd~i_bb"TuYf,=o]z`Z[oIkŊXlO7oĪU{*;88`R1h+%%Egt7Is0[tɓc峲0gUy}ȑH8,]Tnaa!&O\Qj\/  ]f 4;CSfƍ2zʃ"%%ڋҷm* 33>,^jJVUS3gɓ' ///GxxG>&5wDDDd\-{n 6 HIICRN®]t=m&M_/_'BCCѧO"""B5n8L4類sm8::~C`` ܹ `ڵ;v,:w ܾ}ΝáCw^4o<$$$`ǎFϞ=s͚5Caa!Nbcc1~x4mgΜ#((d\!!!ų> 4oeeeBttt+W;̙3Xd x whݺ5T*nܸl޼N+V@ZZ~cر񁳳3JKKqUI `…֭úuеkWK.prrBEEΜ9͛7#==zڸqju<"""'#!??_|饗D\. `ѢE /u*.._xe2"'++K*j4vSe-4[nf]+;;;qΝz)))gΜi L&8p@~jjR ֩yf٬؛5k&^~]oׯJAAAbFFeeeٳE7nX2ŋͺJR\|vė_~Ydfuڈ,^uдexzDDDTG`="7nk'6mڄ={ɓEyy9ooocĈW*ذa^z%[^_~x饗0`qUӲuVݑ-[_Err2._24lmڴA=0x`9Ru?[[[|w5k֭[}!;;%%%hذ!ѷo_?^Z4\[=+W"!!/^4yg & ((ׯqqB&QF7 !CSwX|4HRӧOqyr9+눌޽{qYܺu Yfҥ 899lk…ݻq!>}׮]𩧞 AK/]AǛo)Ŕ4hԩg}]tiCs}̽N/_FLL 8cǎ!++ o߆-\]]ѳgO7'NkODDdQ4qK""""""""zE܉Ȣ1e.] L&=ᅬΝ;CT^^^Xl;h\|gφ; 6l|w3+N!PN'JJJ[0H:s q%=***Unݰ{n8;;A͛(..n>x`]]&Ie*++1c hR=.]B˖-k.ݻѰaC;v /7nѣqMt ƭ[p|7P(سg|qDDDDDDDD&1ek$%%aʔ)2dѲ8y$AoPؚ0a;@\\tXPT?P(xװxbSNi , _|L 4}?i$m۶JY Q)kӦNٳg("**&DDDDDDDDT+L`Y_~Xr%6mjlAA>  2Xnذa*ڵkFۣݻwwDDDDDDDDu , vZٳ2e'O(]v5XN/77yyy'N맧uDDDDDDDDu , qePTҺUhժr-[\R}Yy}@j n߾˗ͬ:wܑU*rԴѸA0A14=8lذ'zwy!"""""""(L`ճk׮᭷ނ\.ڵk!%vaarԶ>)lq^{5tСʴ>(--޽ QT*P(кukiߥK .Į֕zXر*q棯 (--BpO Ӻ? O Ӻp)X,++ ߢaÆpttXt)u"H޼y;BǏs""""""""XΜ;)xxx 55GNPQQA> xy=z:ڵk(J<3Xf bccѠA<-zd*?>.O.O>.OG FTMI7nd=اօi]؟օi}اօi==1Eu8aPDZDDDDDDDDdј"""""""""Y4y}@DDDؾ};WaYg00EDDD۷׿; """ݏ ,"2>S|x{{ϯ ""({]ab!Ê+; """2Eqw""""""""hL`Id}اօIDDDD֎ ,""""""""hL`Ec,XDDDDDDDDdј"""""""""$B}@u}j]؟DDDDd"""""""""Q:(TTT`͚50`6m ^ZZڃ C,(1uaޅ ?#>>Eii)ЦMx{{c*W IDATԨQ>|8r>ݺu _|gϞ5jT=Gdy?믿bǎ￑"8;;M6ѵk4MsޣTVVbԨQ??%%"""V~~>͛TVV/((qqDFF?0k,&I~~>,Y =z6n܈ ҥK:rrr$|xյ"SfN+::ZJ^=Sx7OH&X"0EDDDD 6 .\ xxx@Rصk0t 3 $"}]\RzcƌAǎ#&&駟_aǎرc=F_BBBb&y%G#4zL1EDDDD ''uSoO?Gł I=ⵧeɒ%RJP`Ŋ={{7ٳg$dgg# pvvŋݻc$8"DDDDؙ1cEBBF`͚5P(#LC{_ISSe26oތ0 S:)8._RR"m3LXDDDDXٵkbcc駟РAϜ9SҪO||%3"Q =:/Ժ?ssέPkcǎAAz:kH| =BBBĒmdeeIBCCŤ$`[zx[b6mW_mD|L^+A}U9LVeL&: U:!ׯJ6Č*׾BBBSuʞ={V0뚧=ƭ[qƙ?tP֭[zۈ\hfFOn*͊{ո8qիժ^4i~M2Tْ?CPW6zJ'""B%݇>ŋ=>;֏k`c111SYSNoGhh(HJJBDD~zܽ{Ο?`ܽ{SLA'N`ո~:RRR;~xm%$$`Ȑ!(..L&СC1dj EEEHLLď?BpttԻtyy9F-\.ѣ1h 鈍EjjTѸv^ysf͚UyrJ̝;oƍ}iӦCLL mۆ\СCܹN XO_e)DQJ={رcB7o"##8rީ`EEE2d:@=nĉԩ Μ9ӈŘ1cgpZ(aggiӦ* ZR[nT*<ӧ6mb 995'mTӦMÚ5kw6XNgd̷a_Zua>:8;vL-`oo/VTTI7nmժxi2'O[h!۸qNQS NNNCteff7Arxʕ۷VZI߿_u8{4B^)K1Ǐۖ(bjjhGr!ixEo.ڊ O?*#>c2w\[Ç!-##C~gϖx~vʤ8A[2;G֭~^4. *J#DQ=2F͐ &H|Gծ=*J&N_{# wi'NHkJ%BXi8XDdRԺ?̗%mi2YsxŊԋGFF⩧)ӱcGDDDH?3~Wݻڵ믿0:.Zz5rrr ֯_=t˖-(Jl߾]t1s=O<7c>TVV۷oG֭>|8ϟ8|0߶m4) .iCRaӦMpttUuM{]ӧ-ۡC899UyҥKcbҥz?;r֭)/Lr={D= s%iCծokkmPM Ad2d2;w~dee鼗UL`cC{ƍIYYYHKKN,;tPt p `KFcm[6loÇ7zAE -m6ܹs0ydɫ+M{祸 2e}ڋpqqҎ%5ӽFEEAb"1qD@ff&Ο?olvd=MϟGQQQ7Ӎ7^hԨ}ZT[< uBMD"EDDDs6P\ w$G6hP&,cǎᡷ5}Zl)m=fmϗZ...غuɑ 6ĕ+Wpԩ*k9r6Bbbvbc?|0~~~F;x`|nvvv(**‚ pk׮fOHHN\xW\1Z^;!t)-k؁8r]___̟?ppp0+`.u󈊊޽{7oDo˗/?x& `X`$Q7oެ6 /E Q*vqqѲiK{pmi$Jvv6uSNfSSΝ׬Y#M3ky氳3ZݽQ>xM4_|W_}eee矣YfEdpm8a+bd̟?۷oGzz:RSS1i$d24h Lv=3ªU0* W MZ;Q{)DDDT3/ 7fm@LM64S‪S .clJT]U۶޽[ f=Z 1 3F*qy’%K سgPPP>Ž;ݾ/@.ҟwWO> ~<Y2\aÆIZ= &˟9sF6tGN˖-'|zDEQuK `ԨQO4 WGǎ㣏>2nVVN&MH;j'ڷooK'Uѣݻqor/mD4r'O6Ñ$=wss9rh"66Gڵ]+.M:zͮO?1 6lڴ ǏvS6MMkݺtDTxGWZe\^^6шΝ;KW7o,}AF2U7yd)qlٲ2:Zh!mtŋKflݺdBL6MPeF.]mc#GܯV.]T_"Kd7>.ҟ ,ܕj Q ԿDF뤥!88fY,z޼yiӐSԩS>}|#tηzK^+Veˌy&VZݻwWyJ5bIrNNN]ތ}Xt)lll1Hœ9sW#GJ#tR|8`APƍ@BBs̩rL]ᅬX?M6ř3g bk޼9L(ܸq{kN:Ν;ػw/6mGGG 83g`ɒ%x7޽{uPTqyfTVVB:mXiiiHHHc(--իW8\v Xpaun:t~~~ҥ PQQ3g`HOOIøqj|>nʕ+QVV0|3ftk׮!>>;vֳjٲ%bbbugkk0,^eeeìYЫW/ )) ׯGee{FP`֬Y׿b_7***HܼyǏ/RkAdD:@zZ^QtwZf5[[Q͝;W Ν;C_z%Q. |hB/r^xh}L&zʒʆTٺlK#--M֭YNܹsvJJJę3glC&Щ*T*ul޼Ytvv6+f͚ׯ_ERinPPa7%??_իXĐxb[T˗/7KQQ/2̬>}NRŋ<M[驷kbÆ O}:Zj\GGG 0?Osչ4n XlаaCڢ}3g?gyF:c.\{|Cm۶PTP(prrB>}0|w5K GbΜ9ի7n ۣ]v>|8>3=zT:LXnO^z r* m۶Ř1cO?!%%Eg^M 8s ~GL4 ۷Gƍu_CŦMlMB۷7FAP 3gıc0rHckk!Ѽys9)e: sIT(r![ڿ""2_`3),lݡe}]_ܹsbŊ)++ `oV\YQYPUy4_j G+:C ah۶-v܉ƍPߑ>稈L`Il}اg\.=}P#8 FDD֭o;;;)֭稈BHDDD手S1'DR 9gQQK/=0}bǎػw/ 77w܁CFdŘ""""hXw&Щ;3EDDVi0`@}AB"2 Zuy(Qec LFPNoPR#""""}3r*g׮+PXx΄DDDDD55E ז_}@CDDDD&ի-@EEݵYQޭ67m۪],DDDDXbȚ;ܼ wݵy4lm~&M?oܨX59wNS3.h4P(̯I`],DDDDXbLC:>.U33SA$3}` XDDDD3+) sޮnI ,""""%&AEpĽXee}oiTj߾zu9XDd(1uE|7,4ׁJ״Tw"|Q-0EDDd Νzkv/wiQZI{#ڵ~LVYPXXDDDDD5$N=Z 'GҶkq#s5 2Zn`V{cwBN#$"""Z`>|oKb=~^XxoQ?U_(/NL ޮ,A:XDDDDT'"""7o?PoZ￯~zQMGG`Ju) "B]% #^5I`L`Q`h/Vj=Z`"(ϪB ( IDATBDDDDD5$TcdاE{ _TU%40֭jc99_V-вe͂,""""L`YeˀRO~T*HJRS5^M~yzm[Ʀfq99r=FΟ?LLH)7;jaZ4kNBu~͛3g+gk o֞jt^B]߁>}jSꟗ/׼ … X|9ЦM5B1c lݺ7:zq-,Z-V͝R-G=?-Z$%vڢYfׯ,XL;>A{9L`I(wTاE{Te^(J̝;#%z0EDDT-\0:9rvvQW倏O}_WquBDWMiEuD ''xyyaݺu[O?ѣG`0qRxAر#FܹsիWwxSeIEQ^Pʒ)! yUD|UPQ@T@^DA_eS {JK7=?'M6INuN<;<LC\r"[B"""GyW/JڡC'm?(i+Ca@~Z5icF>`U6mox%kԨ̛7:GxH>q&A_jժt_^ᨈ XDDD2u*pг'jU<ѣҶQ#m __$xzJ#N#$elڴ vK/UF0x`ԬY^^^B͚5 /`ǎV6wD[.f͚O?EVVVe,33GnN___4lp9cǎ7DTTj(kضm)dnB y&LVZL2kժM>u*U@!88:tŋZT:tTYYY *˰@y1rH^o;vQN@բRJӧuɶonsʔ)#G`ᆟFc@+C`` Z-QV- ӧO/=<<Э[7cǎ]pGFݺuF/Ҥ0qF<([,Z-ЧOBAD"`ޖ-QjEQ('(XQ_x[n# |Lu(ŸlY?;v@;vҡ} o{9Cߖ.C qECaÆw˕+gM+dvUfyzzg϶Ovvꫯ|A|G_.˗/(x,8Çaa}?Xh -[vލE!33K.Ezz:~]t =z@zz:;Ǐܹsq]>]tAVV4 v.]RJĮ]l2ddd?1d~>|gyƐVgy;vDhh(222p lڴ 6իW֭[xxM|gŶЯ_?ne˖۷a]ѣ݋aÆ!77СC<(WΜ9 {9 QF(5i}EDDt:q)СCfD.]w^@Z0`ԫW:gϞҥKqlڴ }͛-Y&"c^@V'N֕Zf >C/ -[lٲBBB8M6ho6msIOs=;"88ϟG {cԩHbccѩS't:۷ ,)S#r9Hʷ0Ü:ȱcȢpi+x()]Rd.Qt߷|"/t$,?~~~bNNC]|J*gΜ1isIB v˗/7ic2i.FGG{ݝa־} 8j?N޽krѣ }fvz!Aįڤ<KT\ϋgϞ őa(۷of#zXjU vؗ~o>}}90G`XN0awA"$$-[ĤIhaɓ'_r1߿?ájQlY<䓆5"?7oJ矗֗GC]*[ݚK ߷""-G`xaZjh̙3H#\/^_&m֭EO6fgFMnY&F֭[Ks"!! `ҥh׮Lj0<nj ̟??1c ֭C ,ܸqcÂE#77Xn*Wl]Ϟ=1n8}k׮|]0sxLO?dqTR%:tնuAHHHۮ]yӧjX`a?fl7R0&MqyzzYf68rssoի|biӦY]/Q7̙^8q"MDDD`…c!r,`9Eaظq#.]/// 55ԩS 7n؇NCxxŋVky_֭[n߾@?Dn0a„x"DN5S洘mo) ξS\To x-i%ng-c6ye_EŋqRsv=*r?~硡xN 'NcqkFO'}ر#*T&_]iiiX+GHLLGg4h!.Kl/o>BCC Ǐ/+V@NNA0LŴDbϟ%+_ԬYݻwڟKYɓXf V^իW_~̙3ѴiS4=x S-/n#oM///C1R? $Zj e˖_L8YYYXnx $$$8{,BCCMhӦM3kǎ=z4DQD޽W_bŊHJJ{goӧOGÆ CD6f^{ 민|3hH#._ؾ}z Ss矁z6m/ +(-r #U6)5_JeEtyM iELL yT>pjժe]fͬSbE==׽{ EPY!qN>vQMO=>a׮]X3Y]vLL :[nM67nzb/+Wbʕ1~xmQ7nݺk׮"##dho"gx~B h׮RSSvZ1aBE4j?3<<<!!!7o.]7b0`~"" LTc/]FKlT f&OF- ܿg]Z 8HN.^Jx5ҧykS̵~vFvȼ YYYV/i;wΝ;eݻ_|T,W3LG<i'QL|9r$z=f͚Yf|hӦ ڵkݻnݺf7~o㊢UqԒqaݺu8q>BѠI&hӦ :v숮]Z-qW"((jB1|pԮ]񶞛~ocnޛD).x>w?q)cǚ-N,\vͮT] - x]Cy}[mh ^]ZO18%~A2 uHachhš0Ļ(>JQ X@۶Ҿ<ʚ}ieQQy7j$m]ŅIh|_-[}O:cGٳ}Kgs&]rX}ըQ44td*Uܼyfa\3^˜;woHH~iL>w˗ &^oP:k`)EDFFbԨQ駟pMo7n܈?C Q71ncޛDJ` y{{C@ӡrʕ+7Ɯ$\r8{,,XM7{<Zl1!!ϊ\E_51`\[S>=,MNtGg2)`%<&BraѢE.l߲erA-BCC ٳ/r_[6]H}OY;=7o@X6'''/TX-BժUDvv~yuQ:: xnwv*U>L8IP 'O⯿2Uݺu1w\;wHLLDzz:6n܈MBE|Lw.|azzɉ\7ܶ$ZC.`xΔƷN#ȉt ]v M=UA7,HS"M=t6mG [˙=w>"ӫW/~'3 SlM\ጁv†  xgyư_Xlwm_:F W+W,,sAws5bcc Kf̘Q*? B }W{V~i&?aNNϘ~,`9/"!!wARRϟ???,^۷7C=p@90$ >>mɓYQxzzL:dOs؆m؆mS'_}e&M RS!xzZxi^= 8BJ 6郏>ytuZ:nC #wڅvY]v9=zW^1;_x73:t|Wo^3gbƌVG?%''/0Y?ƍ }ի"MFȅ&ifӧO7E3 ySOFm޼ӧO796##;&&ܹs͎ܿ?^{5,_K,z={TF 9py<䓸yžrssiӦ|#꥗^ɓ'-C̟?pq~FNL:l1… x]@SNOtd]`` FƍUV8y$f͚>㽼' &&غu+zm_<~ž-\}Fɓ'<ְp)6Q shy$lm /&Y~k}@Z_? Ç J\A|A)^Km>Z ; ې*T[[n|2<(m:uBDD|}}ӧOc˖-)Z į~:"##1l0l(bϞ=XhaW~0pR}UV!&&iii?~<;ׇ?߿ .`޽ؾ};zّH~z\|M4A޽ѡC/_8y$6mڄÇc톩mho8< >}Z '7i_|ѣG#99={D֭ѣGT^:III8uq1c=;t0a6mڄg}e˖ٳgpB\t ݻwY(Gxx8%K )) ͛7ǫz!-- ۷oO?@<6={~!^{5Ġy\2|}}x\&L0c̙8rcDDDo߾hժʕ+͛8raΝ;+k`,X 6DРA ''gϞʕ+ ԩ~J~oԮ]'NĤI.] 66:uV`[nX =}-Xr"'xBA| u\ZZ((8k֬|ꫢ bxx>7o. 0\T?_jߺ6GKm^~۷KbF(b˖m#G9(%"BE #QرcEرcw8bQ_vP_>4+++K|h!C<0ŋ m f5v[mٗȑ#c=fk#nܸl?K/dF#ܹÇ|1+W˕+gW˗޽k6K^^^޽x)_{[ݻ'6mjw bO2Ů%~cƮ :Ԥ8C)S|r_/22l5iҤBeNa~d!5j>L".^pے%KX@ڷ];Juc-; IDAT~'rpL>]tA*Uqxj*\raZ1///ضmbccQzuԖիcŋ },17ʫmWFpa/0`jժhZ)S2d,Y7nK.fķ~`ȑ_> 4k :v؁]4n#Pn]ٜۿ\p_}vJ*A hݺ5^u[  zqa :*UVE`` ڷo쁅y- F||}MFFa㲧/{8Ɯ9sa:gA\\\Yz.]ct `Æ ,1eȷNO>i1pf4U-֯>L*Ni4RqL}L!i`D`T<0r$7@ƣ~1vX̜9SpJqNsq5Z֒[b޽`,s sѩS|שS?:YmD&/PYJ㉈NaG`y#Vt6[M͚Y.^şJş`ʷNI˗++WŋW^OHG6_k׮XbE3zlݺڵ޽{!><3hp 8 $ذaAi %"r[E)`u,m O?݊)KNlټ}ׂI=z @!00ؠAo("6oތ͏>)))T7nƎk۵k GƯ_AAAHII |w1hРyA0\aN(;HK1DGKSO^ tVKV.^fr|FE?wfB"""""8 TT ˗/Lj# 3#ya~FZcO>SO=ZjCdd$FÇc5RF]v_~ CZZBBBУG_|I=""q.Ye@VA/*޿ۧ\,DDDDRt4hPF8-[ƍsH-Z?쐾TI>X40ڴx4uX性> @"0Pzy>p};""""r{,`M.15 9-"Va#6oRSg<㢕,gf?0},B"""{S L.>?,\bE\͜9ҔJ o=1"""""8^E9ܚU Gy!!DDH83Z5p6ib1( @rى,"""{g EڻwքWP4U8 ,"""{g 95k=Vc""OGt%۷V: """}vC '$klbN8S```l`䬅.O~ """r,`٫8SU@t4PR裢=3qXO<61vRJ>̩0ɞ~ѥ%W -,̷:9-5Kݻ/? \а\.<м9w8'Y|s.̧zsisZDŝBX&xq|,{\>*S}Sua>\G`ñMD x{KII@2##zSDZW!H[""""P,"""{_yxAAEח+ o,]*'Ms(9-a<}P?b\7^"""""< !=XE]_̝+_l=L\DDDDX"""<\9eP??/_?9#J7.""""rZAD6/H\.k_Z*`q|U̧0|)%o_n]ێW.""""r:,`ÉG`֭;"""""#,`M(*9sZN\r|6oowKL0Ü I:X"""\BhV.mkڴɻߍ XDDDDd,"""{k`9,hWXׯDDDDX"""O!tI@zgm> (Q&6""""r:,`كS+4[ٲ 4j$]4B""""zDtDDDN/;HK911TZ,""""2,"IC cN I^ R63TnZR]>>̩0D,"""[!!:K<1 'GX)p"""[xUdf'N( 9&Q9-$'?4o.(@utş0|lK_˖ XDDDDd,"""[8EDDDDFX"""ɧ\:~HKS6""""R XDDDp aX\W:""""R XDDDp 28alABr)ͧTO7|s.'`'/`\ڵ x>`fͤ-`ɒ=|L;LlDDDDTX""DQT:r0F8,x=i$ ##[ޏU&|)S}Sua>\ XDDD {xAA^}V ~꫼ۯ]*,`),u^^ԩ̙Ҿ>̩0D,"""k89sEyg!w'""""UbN!t?tΝyO=+(&gR\` J`] Jg'|-er0ɧ`>Շ9UuEDDd :@X<Ыg J XDDDp s)_xmiR%i[uKıEDDd L!t;o +K¤- XDDDD%o~hPF`;w""""%[Hƍ ecocGi\9@rs|ED6 t`̩6o;+ nO_ț⩂inOc>Շ9UuEDDd(bb3!OTALEDDdk'Юѐ-\ȝHX""DQT:r0諶m__ec X̧0Ü I:X"""2] XDDDDd,""z`vipGX""""R5 o 5 4Q: XDDDDQANec!EDDDj,`eW,""""UclASm*O\ec)&S]OaNՅ$r,`KO.PBCmNl,DDDDp,`y{BEN#$"""R!&Q9B.~.2|D%`1|s.'`Șq\J XDDDDd,"""c,`.T,"""cr#<\8 Μ[\ XDDDƮ^*(q+7hظQ٘!X"""2v䈴mP8 X.DDDDD$ș~̩#GF*K!0]| >̩0D,"""@VDD( J XDDDDd,'qL0ݻwGDDj-[bҤIHLL1zhDDD@! Z·~1:u Æ C*UhXQOȹ>,m74rVb"0eCDDDD#(*k0֠ hZܻwP| ?]gʂ^DGG_z5 l@;66˖-kqȥ ̙̚t4TXYY@suN)~U?~$ZjsbHOOGRR+VDjj*o2+)) < Q^=۷)))HKKÜ9sm6?1gϞEll,Ѷm[>}Crr2>?iӦ@D(yV&AEmvYjBDDDDX.Ю];1b}Ǐnj3ǏZ֧O &@8quwArJTP'OD```_y̟?~~~r ʔ)c5VV%"PX4\S@0xpuHXrb999HHH… ѿAcǎ6Ǐ 6lh/'N]>޸ʡCҶJz^~9o,""""U*ƃ3gbȐ!n~aRJX" ##7L/S lPgBj!瑫 sj /|и4+5% X̧0Ü I:8 UP F#ɓ믿iii}___}g|oX^7{)T?DD%N.`q:L(m]EDDDXIx"p$%%xboJh7OOOSBaaRi \(7n|faaafԩ?c1b_'OĬY 322,c|1c7><0{y!b 8G!aNi[|o<_|k6y?>>>qzsFn}oǏGpp_""%8<|)Tt4HyF1V|CDDDDK-OҥKYNC`` 233APV->ك={޽{RYYYx! ::֭?F֬YA!++ tô4Bbٲev=Ӡ 0*Ü{m۔HO BBG1lkx 1XXЬa>ՅTT]OP,'PR%,_#F@TTQ~6[GŨQPfMdgg ?8͛M6Y,^O?CaȐ!\2222:xEDrz+HJ?zXr""""r.Eʷ)aNo)4^h`(믥6mxi_;<6LOua>Շ9US=9T8laNU,Eޕov<Ы4"ˉ0|s.'`ŋ@j*ԭt4hy7oJ66['/ο;9}ɣ6t:ec! O?mջ7p$0` Rפb9}:$m]x Y1d0gN>6o5/` kW@Εƚ8Q*lS`ܗ ֿ"+ka5yTamв%|1^E,`G`5ilT<=_z4mױ#{7JדK>6"""" XŠqA9rg U3>%-s 1HHAN˜O}lk $l\0|s.'`ˊ'N`ʔ)LCժUѼysDEEz?$""9rD֪lun-[-nIpUEDDDDXo)Sp|߻wí[ "DQիWO&\DDxyijv4y9 ؽ{7[nn_p!ݻի㯿޽{ѴiS/T"T)ÜBU 36+h4@t4jSOua>Շ9UueŅ :u}͚5?ڵCf76mTqQpwQ,мy@[.`)>O i{BӡW^ۣsΕvDDTXz=p挴 ʕbbo/8iht:dffСCh֬ ???Z""痜 JaaB%/66/,""""XVT\FU֭[k|E-""rRra +Jك,"""":t(;v,q ZW=z̙3x!W@DDT(ra0E rBHDDDtX⭷ނ'֮]e۷ѸqcXKcÆ Fuyr9nS0| >̩0D,+֭5kք(E[իM~}h%B%",S!&&ΝCbb"t:ʔ)cFc͛7W J""*0!O#2&8iePt:tС!*e(*9TeS>&6| >̩0DSp !Y)DDDDN#씓g޽{V۶o߾"""BF~_.<0X!!!ǏǪUi(999ʦ/t l|t=N!"!!-Zeːa8 9DD.Sɖӥ?r*!`ˊɓ'#!!AAA={6._ 77ꅈ-Mk99@AA7 IDAT\=?˗/k*U@Kr? (9TeS>%%&Fn\| >̩0D,+nݺ???t]Pȑ8ѹ-bˊxxx*OD6BH/\㒈HQ,`Y̙3JB(@}:XnϒT}q&bGb>ՅTT]O"'N}]C!""Gzi_%,*!ѿJiDDDD[.~7l߾;wƶmېtXDDT+A D(""""ROgF E۶mC)ȈȌj=,bHQ,``<'󣉈T@ d}Xb۶m>g,$"rr*[J>XDDDDbˊ:(SҒzuNVpq8[糤R| >̩0D{B* N!$"""r UH999HJJCለP8 SG`!==fBfp-Z $*1V>nSN!t|4)C2|s.'`ˆSN!22o6<^Q!"z=ߏ1c 22OV:\""S0G`͞l,DDDDnSHMME׮]qUhZ111Tڵkزe VZsΡk׮8v,B* }|9)/pUTP"##Mڌ1GA=p|8q]T8J] """rsBhood7w}""rREa>9zT8 XV;wٳͶݺuΝ;W Qq ,*.KDDD 6j4xzzBחBdD˞r-nSr|SMK!Oua>Շ9UueEʕǏl{1rʥ(XsG8 XVt(bȑʲ.33*SNVn*G`Q 7*VEDDD;v,<==ƍctz=z=.\7F||<<==1vX&r8k4--o #63|yz)TS}Sua>\VYDD.]ٳx饗LH˿t:.]%B%""{ȣt:GX5 +'],""""Ep Ǟ={еkWR"w={ GKDDVRaEDDD(CTT֯_dՅTT]O"$''/QSvGRhe\7?QXDDDDT*JLL)S߼y6Oٷoi;ȬpFdQ,#rJEþukN_|ƍS*|c@v9 X)G q|*EFa%6TS}Sua>\G`6l:tA "UVYŦhڵkǧ#&""ܽ+m9}+"8~\߲EX XFUjժWZx'M#qK""""EeťK,rg T&""""75H8%**kהͰe2e 66fA!$$ׯ/ȈP8AKh!w""""ʏ,+VX 8f 99+V(ȈJ J@v9UyեjP#Oua>Շ9UueŞ={hбcGm BrooV-`: v`m`` q"=ݻwh" <ׇPreWxŋhl^nj5SNaذaR t:QeDT>Jٌ@D)DDDDg!<^/c#''4q֬Y5k֠{}h4Z| ooo^ Bvv6A@`` Ұ}vl߾֭òe8Mt`nS.J}[/X̧0Ü I:8ˊ+"==ΝܹsHOOGXXX+''-[ļypyΝ;~:F X~=^~e}TZ /mڴ1{ٳglmOƽ{>?bڴiEznDD}N!$G)_^&%)`ˊvAE̘1f[M۶mXqqqؽ{7^~eT^p{xx8KC;ZciZYed)2sIm15%{*{JD4325qTPQ6eX3sg|ޯ׼;g/s8kN4 ٨Y&֬Yn ɓ'gL6 iiiV6ͩ_AABQfd`Ks=Xd &L`v8aNN^}U,^WZݺu||ذaݻwW\rV c ,f„ LXj&" u`Ho,dL`СC1sŌ3xbƢ^zD!__|Eu]6onݺP}5ۦ^zhҤ >,""jt} """rL`aΜ9Ype,_Dwww;o_ -Zm;GԬY:u3;Dn<_*""""[3f`ذaXloߎ .@QSNxꩧP]N222 ر#nv_GJ*z*/C'|bgZ;88˜Zj(Z ƥX.՟BM`ri,Oa y0e;SNux'ooo̟?Dڵkc̙߿?7n\;w7ƍtR?,k׮4PW[*77q774""BOOʒ"""":9N^z%PGTTT61113fLA Iر#~g 80|?vަLEQ naat3X38G,yD3۰ ۰ ۰2eJ1P^^~wX]9f̛7СC}EQ0k,@~~>~"B}\mo7nM8B[i1~aiژI`9|lsx\#3 \}}lcqs1vژk1r'C-FJJ PC )x<55]vEnn.~WUƍÜ9s P/RհaCTV )))8ydj׮ HKKCNNN`sesaYӳY+I`s* 2 xqeͭ,$$[Ʊc7TcǎŬY( fΜQFUi֬Y~BBB8@{B""Nԉ0aUV!==Kc+u1c`ɫѣGW|pqܪ@_~ǺtOOO!~z?u9իW!"+&V+l , 1`Μ94'ر#*3ȰAk$;v,Ynٿ"WR> `rJs̘1tLDDv&8C , ;w2J*pByȄ%KsNW >~{s[qTz 7aY'"rgYٍ"wq^^^ Brrrp\t yyy%#;;ׯ_/uN>H[Ucǎ-JLLD \J\z`%EQ0tP|'f=v( •j2EQؗ2}zP&ܸtOOG3u*aEV;-X؟>5q{qB jG]yPKjj*222Q0}x^tI}fff~xx8̙;v8w燆 ⮻O?]0ı4޽{1}t… @֭1|p<#~]DDS6yE:RYEDDDdsL`ihѢlقݻw]vm/_hӦMY*/x+5jKVH6I} """rK:aon߾}xV#$2@2hAMi,Oa y0gAӦM~z7o.Ɔ 0bu]@tt4W#"r$\l)2rrc􎆈8P֮]>}`ݺuXn]cM4`طh~m8l hرؿ"""">f[P^=޽o&֭[p\!"""0ydl߾5k1R""*Ea8q"&Ns!)) yyyY&իwxDDT!$[kRn]8 r@96!$[]nn_|}u Ȩ8ʤ(!LBOGԸ1!'r߶*d xا$rP^^;4jڵ"""MBH(@l,gڵ@L"eȬ$L0~-_VEQgSbEa_iP94jw462~0*u:?}j,OPcKCRRڷor=/??F9~pCˬ"ص 71C\0yd$%%jժq)ܸq7""r"+lEQW^k AKCڵ5k{;79e:5jjsZ􎈈ȥ{Kŋ} @HZ5MqXBBBΥUW ${Cnw7""""bKCll,\[P9XE!JDDDduL`ix Xh<.ѧm:a.џ@MV24OI<иqc|w_M6!33Sﰈ c0|=++d5LpٔY k'=[Ӱ?i5'`KæM&"rjKӦAEM`YYXwwDDTiirW@// Ypw""25oZj+QQL`-+ޘ"""" !йs琐4j2d"""8Rܦ0UcĈJ(L`pya1|XLP4OIU+9l˙?UXL`U`,?}j,O"!DDd,BHY+)11.]^z:GDDDEp!959XHJJˆ#PF 4lшF #GzIDDK!$(WA0Um۶!** CJJJ/_s"** ۶m;\""BrroDDDD!1`!00=bcc8{,6n܈ "%% ÇsDD.LB ,S@^oDDDDٳg#-- 5ƍ Wƍ#&&/bbbp̞=3f)b"P++ ݟΪXOcaX؟D΃C5Y駟H^VV-|k%6""* &ȪҐt̶ܹ:uN:eȈTBHs`YXVS2"^+ ݟΪXOcaX؟D΃ , vvQf;v ++ JV`P+ , sgErrr.^g}зo_FDDʒ[!$= ,΁EDDDd`d.\&M ##!!!xS03g "55UVÇsR`^Ddw2?@ÆzGC!Y3Z5e!""2<~5>&ʰew}PMAAAXz5vj ?8H7׮UWM۩S@d$mJDDD6!e֭ߏÇ#88B"c @>˰̲M U;ph@&Z` xا4~5>V`(hРQ+qq=۸ذy{rJ&p^, IDAT"""rbIXB$Ui 8y$EV(W"ܺXHI'.""""'[UVxlϣuؾ}"#/aIX.M`7w)u?0jT3l:;u,K`xQ4OI<Ұl2_ܹsm;w}aٲevrZ]uv֮s_/sg`6}b$+(^rCʅ , EA޽l{w~$qi^ݿAzUNfNp͛r+;\<:u UVEjl[zuԩSvrVNe=@dL`|NBн"$)\g#XEDDDT. @~~~0""" ;,8` `: 7Yt ̶oaٝ(z@Vf>~]N8EKcL&2d<{VnLjՒs`?i`6/u9RxcJS35 Xpwg*g}EQb $$$ !!lG}/stD!;2Cu&+WsVZʕ~Pa!{xժ\ŋ' ٟF1a?@߾򾺲B?}j,O"!e?Cz DFFǦMW_Wp\Wr:Pi`bY=գ\00x)xrO/:y;`z9j~LH ׋.GIDDDDXe^zW^zADDq 'Cj֯WлkCv=GXp)?GN˴ ߸!K/.""""' ,""2M`Ur;s&кq('6hu+zV`X4bXV^װia2 NNDDDTnL`18`_M@ƥ}UǙ2 &ʍ ,"*R$!TG!W KXO#+c!X؟>5'`*Ξ5y%Iܘr)))Xt)x 4mFڵqce#-- дiSx{{[ƌ3]ϝ;#FaÆD*UбcG,\yyyxDDq&$ \bk;+&''Q)Boӳ I( |||담4]Էo_\%1 goyyy-Z >>4sNs=HOO(@vv6rss={ď?h.ٳ@:?ڷ7_G}i2q\;OOLvQ#_w4DDDƧ :`8~8233q傪(Xn^7n}݇gϢVZظq#^L|רR ㏛vjj*>I&?k׮ᣏ>'6mڄ#Gg@DTar?.;*fMY39֌KX'V3ÍEDDDD%9 7oƎ;0|pDFF|P/ TK.Ç( [@4 .!..ĵ}]$''?Zn @V x7/ѣGډ*'A` "bqwMӧ?}r1N8ܘdZ =+unݺi>>l02){"-]УGtСs}Qԯ_H[}YAzx#!-[f ""'L`X SYS''%7%ȉ2EDDDd1&4ܹC_ⱯxqnݺXeuo.@~~~W? *M>}J:|t_W@DdCڶ}5԰!PJέ(r W39Q7SXKCʍ , /Ʋe_xRR ]?[/@V`hѢÇ!(h޼yWKNNƥK QtL`!pイQz׺pDKO)]lHQL`q!JIk׮! ȱׯc޽A޽ 7n~~~HLLj /~(#mʔ)PVaaJ`; Z_jƍ 77ģ^t`TҺc6l6l6lmLRwL2>&4x{{6?6mLANNծ?f̛7СCK)`ҘGcSƍfo'NVq6wx؆}ZM=YS3x xÆ{UF<)X֦zu⫯J)|ߡbf 1׿3ha+8qb1l_~@y&QjU\{ܸq3gEYK/mWv+f|X}~xzz[|""M7oayr8ߣc _/Ykwsr2MONCݽd|L`iݻ7xo>OO{o@~~>Zaޱcb֬YP3gĨQJmۤI(!8Pj;PTWW?ЬY3j{""q钜p͔%KLP\_l{}5umGV[lfw""""* , GFPPoߎ6mڠvzQKؽm۶1c0{ѣ5}כm#? ٳgǚ6mpgffboEzك:|F 9\ !~[65e>'N(1RW0ga :M6> 6mT׊+3f BCC~aϞ= ,ĉÆ wQGDd38e0:sqPX"z`O>Yf0m4L6cǎ-RիWW^8s bbbU_=_HHV^~СCh۶-߳gO|NB.GgP}.@a:VK ~}""i !4؟>5'``t-339n6۷*4i<(;ӦMΝ;L^\tt4ߏ_| 4@NN `Æ Qׅ'"r$:T`= >,/6]CC+: !$"""*V`iزe E)vZI;#wyBϏܹs+]Pqܶm UnWuy;WV`B"""raKC=,J`.9Uyyy XVm¸89բEc);ƶo 9 $O#b y B2o>>>x'k.DDD2C ^{ S΁€(iB""""KæM4@HHxxGIDdwX6B l*ϜBOnKY]l|Lr BHDDDd1f]4t]H +mI~hꗲX`l`RyA(e!!DDlRԩVMtxyZ_,AM`]de cV9%&&ҥKPԫWO爈\  5oelON~˗:t7>% rs0ºuaIII1bjԨ "::hРBCC1rH?^0lƒ8ɹOmTu2w܏ƌ&M%\bl'''(fI؟F>5'` ۶mCTT͛+^|sETTmۦwDDF xiKV5l&_u QX<&4$''cHMME`` ƍ8:t† 0n8TZ)))0`5 ›7V%cc+uZ]j.~\Gr_? /Oب 6!A8XfϞ44jѫW/4n7FLL OqHKKٳ!TdQ% f̉| ,s<5lP5kP!ޟTiSڵkSXG$^}Gi_׮VSgmI/rw>P1XEDDDT*&4$&&" ;w.mNSN!2""*X _@j/^ij> ۷]b#"ry/m%+6nj}g9Zd|A~}];1&XDDDDeܬ7zh,ZDӦMs!&&`B3g >> .Djj*VѣG5)c>RK-]WhpAV+9 K36V}&HPޚO*4?Xñzjw}HMM;#iӦi~a#T""S!BdgGcmjSej%#2x]98XDDDDe2t  !D[HHy$$$['"r' 78|BB5M1ӫ\Y14T˓}ol. ,"""2u 0|0ԯ__lec>DoogQQ"YYҟ 54WZ)E:(6O*4?X( 4h   r!E/jUTcspaa2'Kg""""*sR*gܹe%nI'X{_q-B55~!Anϝ3.!o,C̟n,JJ2w侹׭ƚaDD䲘a_rssqd*dxy8}V+-4(61"Yx,V`,04vdS<)#^l)՜;Wiض x-N`ٵKoUskd̙@f@vy IzKW.YX!+š6~HOL)ueYd y{/]61tl;VK' =|h={6=.-?(=5k kyD!&M3?_>}??E) xy|S,'O ѵ))Bw)ĵkI6!>DٳuBm+c5V_s7Qom/kPcj9rPEL2֍7DttPEL>hPψ C=ɓ[˗ !J&v-t"E"99Yp?8N_ )&6ˉܼ)֭gw4$~hR Bt$Ě5֭Bh!_\$ܹBi#ĩSۿ_ѣx%!ZbcNɊ>Ӕ[d|NS\i#ȭȪ!CLo2UiM2gի}MZI7sɲ ZUq?,SO3w#"=!Eg_bȐ!u")) ƩSp!+V`:GK׋lb`t=ӧww9a9+Ӂ`- '$'{n~CK7?_ΟUYii_SKr.?+#o/'O` `Ӧ X {NTX@pE9}yT_}U3G96Eؼ\V>LNIsmEEsYA7N_6]E{q2yUS={WUʄƍr޽xt9R~nT*']AUI󡡦J-RDWtz^0ݯV xU9',K^Mu?.Wk7ONߩ|Ç^fG=OݺݻW$""28V`Y(??+mۆ$f͚ԩzwwwCt|(Kt>m`㩧ed¸q6۾]ں5v\t7T?CBd!I+FtcW7ؤII[VVU.WɩPY&Y\et`cGȕ?\B%? Nt-JVݸ!z۶}!nB&`0Y,ii\;w;}7;.>^9thc q(ib8 777tE?^ԥTl?pL^"2 ߝ/^d*X囬 -eRl,( дܯ^V*KN2 ֺXN^.pIQdu_/ykiӢ\}Pu{~Ӧ2Q"'YӺ90&4!0tP|zBD8.]2)S>J%XDV-`(9phSR2!%ō*x/>>wg\Ν[jӦ  5kC?EC*ӔN`AiXv;89qj?T45}z-w'עL ( , K.˘3gL]"peưt\˂ GiSY@c^ժ֧V􍫼|}+(o%""I5=zx뭷HscN`esr} T C\NY39Pr2?zGCa0F %q]M܉\XEAժUP2DFǡS>[`Ci~9qJ9-Mnm{_rA4&\czi#[dGI}O|(L`ɣ`8!OKr֠fs&*F-lc{!Z};0pqX Rp "2$5y4z4+}Kțq߸J$|"" ,"6yy_3f0>;""p6i"r$' /]2-߼&@ӦEEV`Z!Dp}L􋇈8;iySYuu[>mذ/hr{i4eXDND]P&""2 &Lkaq, ?K,mpYdffBq ;FGDdcjVzmBpi^,[(+WuСr?& kwDD顇LOC 2Z O>$233H7Z [rN}z=)zF U+`^`F'll~>pH`%u ,kGdaL`遟b|!`d 0r{w^=j,O"!:ׯ͛ ĢE0et 9s`ҥzLDT>ݦ`Fqu*,3:A}5R,2#57"r`Mb{LO}!""&4{СC?駟k͛7cڵA5Q9llڤF+Nn˕^[77$fk~UkT>L`9EƎ5}ڥo , Ν?_n)'VXa*gnܞ=e {mJ`U&YYEW+&Kk.M`@nry*&MT9M989ĺfM /Ȑ O/l͛7GPP;f*..Nnl)XF1V`))SREYVzM5!"(*,#$""bKCxx8222_p,22믿r "](ö9)R43Q|ʪTSST./_MT`ݼY{T1e5'Xv?XZl\9rXΝnSLA~~>HDTa;v'BnϞծLms5ժ%梊SX6ID ,""2 &4ZedžEQ7ߠy׿mLOWZׯ_Ǻuo@z777曚ϝ1Y X~a 2 MLܲeK|pssáC_bϞ=G}#Fеvڅ~aҤIXz5Μ9Se/Scq]waʕt~ })s%F26e G)wZKr&K*!p?zYPU|AW>=}.hf_5g@Ь/\e j8 Qca93p|g%zꅕ+W̙3 B>}гg _KQM6hݺ5ZjQFB9ԩ6d_#F?ΝZj!55.\ӧy0u1U@K(\23,֥ڴ xyl2az:o|DDDV5i'Nt邔b+x?j2~x!o  11?3&LGyq"rR EϞ@p/e2MOWO[:Pwイz˭žeT`m'~}Q >ޮ l(m!!2DzJMy{wQU7!!@BE ꆈV.E* ""Z.QۈU@, d k [ Ⱦɽ3L|?ϓܹL2=Ǐ7~ \WU%ҡ <Ѧ 5$n-Z.!$1c,@捋c̘1Q^`ԩ3f# >RW]u>#F@LL \.XDN h=GEiijQwY+)54uoXo1*xQf`H|}`֬Yعs޽M0'"{^UYĹ@2d ,%@pvj KWdXD6%wX |)!"" }eFw)Ç9,O"`+H9SN!!!ݷo_K8x JJJ|g袋rtRwsZˏ}׋ŝPUUFAEDƪ5Q5Hwo .NO}e`|} Xed4}3L$50Ed3^6Oa` `FӆFD555>cqB?~!u9`AAZyrPPPPW}ĈԩSkѢ&LQFaԨQضm.\_W3ll^?XhQm_/Ode}"h2/>N׮P[p`)\5;dd@⊦Y孏g%py]Bh1|`h(}$vXF>;&vrk}؇}'>K,q{I,;v7p;W^^^/wZl'|&L@II ֭[nz̹w| 8cF0,\RW}h}7+}|9Mh].pۧr9227fM>Xw6_=*?h[a<Ӽ> pkP[+^$ˁmۀj>)'96r<1o<}}%Os01x`~uz-DEE[n C\\)SM6F 7`Æ  GPGXXB"j`dqt$v!,(.?Dd37{d4z\uz-`loQ 77N555w. c\*DDԘX⫲R}jJ@(*>Xo]lwo: 68za5~z=fۼysqO?Tƍ@dg}^_'"m 4 PӟmǎVll8@fպ5#T|"r[{sADDD `1zh >a4KEEEᘘz>}-1|Q =Snݺ1Edwrgv7f`jvQ,Y8Pn2Byy!2+E YM`$ `YH~~>rssq梶PRRRw.77%7n܈+V©SWUUaݺu9r$n EQ`;> Sĉ[w;rt@VкukeScΜ9x衇~ߑ#G_}݇4!>>֥<M֤OQw"$ s2#e`sK\g|zf`y{]>u HL4f<6f9|Xlܸ!@Qh>ēO>͛7cϞ=8u ={bȑ5kV]v_fϞ.=6mڄ\kÆ ߏ+?":rWV|#رq`~A<X{mgҸ;!YX,X (Ӝ,#rgFh߾=̙u% cW @NNDfU^e:2\2իr_|<0dn3s aUy30j]Z ^-XS?>""0u… e1!d.]:}:=k统g `u a@nnKS**s2/sXkX|e IDATf>}#]'2p+)>X(T @ Kr`C<L2i 4h˗ǎ;[e:Df`96unj|zN.?tx5EDD BŹFѹs=Z^迟 ,fz&NwQ( 'Eul^h+ `9v [ ?_=^EÆEDD(B\֘v]'ڻWWVa!gߋ>* ~^KDJI_srY :DDDc ,?z٤e ""+;l}tm~ib11YS l|S?`wֿҗKVV d8":k }VYYNjcπS||@Ń|XkXj>SoNj^yD<yy,5lO"`+H"""C J W Dڻ{ 2?f`9ܽ'ְajXDDd1 `IVVњ{UjXuc-}۴ 2?:za^"#Ek\0gXDDdY `Ann.NE]dh|3վ=Э:պu\^PTX99eK?ډ\/DDժg,1c (~j ;;7oFYYEC=dAV Ə~XuMZNɓ}2bbgCDaYX~ /J5 `EF'L^xA.LI ` ,#h!2DCGDDbw""(^oѢڶmcҤI3hdDR;8&˔9r "8v 8xPJIJJD},) |J'$0^{>cc鏯bdVl7>70{DDSY)ZրEG?q’dN `\>ئ -6Vd1Ed ,f`Ű;Q0 "]Bص{꧟D{yAy+Z_5AE{ ""5i `62~=p8NNtq0\Ѳ&Qb1,dў< ;ݜ_O"F(++CAAdwnЈ X]$<))b'~sqb8rE'XD!];68r`AgϞ3m+%ܾ=>B0EbksH}w5\ch 7W2X@Ϟ_6m>Di),1Ebk掃HKCll,W/ /Od+<7Neg֐!baX `'-s Ѯ[=jPȖsJ>q>,?:wv""Q]2;J,  hg_|QgO=f ,1Eb.7,0{4DDDCII njP/3S@I8JkXQ3K#;x/_\hD`~R掇 `c!11gF~~!2 w״)΃'O\9mN|ŸTP{?Of`'msP?O>ih,ŶsJ^q>EHHH_|oǬYp#V%èQ !㢵J,_DF|XGssZ;,5s&{]f!)) {ŋs\P555ȇ\X 7x˧Oֱcy\TXD!J.>|¸xKQPPPwSehX'NJX ,0>nXɼBT"hU^:tl(c?Νt*QYYZ_DD-ˆ,P3RRѣY(2U\,0(DDD  L֭(HKKÌ3йsgh5"3gcQPXݻ&?P-(7$!:d8(1Gqq10|pBd*d`aN>¤$ફ̜Y٪Zg',Zf`ձ'}0G=P]hX2d H?Q3޽9s_(Q .N3Ebd޽掃BX~|(++úu Qi &lrԩ}M5&'KqXȝ(DɕW3 LsEjj*f͚M!kڔh E `Y}N q߈V~ icθʈ"ݿUȳ9jp@U0٣)|+3gb4hLK.j^~ȇ /O*p82Ҽ15̾ڶŹŒ623BSO\ <0d#""3f̀(uQ~o(@]6x57iiYB(X;`UJ \Ry e `!CEQbˏM(],~I@Dt_b 6b\ `YD+=U&%""2X~=z!5ޏ?:tm߾̙ `Mp7 XD!np :v.QG'DDN?pejsŤl[ ,sʼn, \~8^ܱQHbĥ6};0r;v}7ϩ ,3b>e??q5&6=qQsJO"an֬*+eeG$m$$:` `^XD`x~%P^nX(9-(Сs``Hj.O>Ν', `u&jѻ\G$1ED 8ycYho߾nR,reHa|CHn xiv 47{`8q3TWcrg|VQ*YITP5$5j IS\Ek &":$"9OZMFpE/Zw_IbXӦ7df="""  `ى6%^edv jV6zu|(&&;.Rʼn5 \| .!l |8pZl /#F0{XD R;V۶j?V +>^"%BXV]w۶tFQQ>(爊g۶m2e )K.|@( wh m֡C#b>@мka4GNcg|VMQ iVz~R9v> `9vNC>PO>3 /x ^`ĉn+@leL4ɨQf`Ogaa@~GD3fо9sFdab YGt4.  Y@") 'Q`K/L6~۷oΝ;; Br5J,!?DEQ  Y@L "w2w,DDرcફz}ŊI&a}A}{ u$PQ!vt̛'udب>.!$"72{7P]mXHHH@II ]o_|!^dV׮u lPDJf_unߋ r""7={-JDDD:aK /«v>33r/ǎCUU6ƒdKf` ?!ʍ$YD uX\BHDDMƁ DDDzaKce˖gW_՝ƤI/ 0HQ@0r̩w-52UK/#N;8~NC >Ҹ馛0j(TWWcΜ9HMM_^WD\\.̰Q t 6 0ED,^ YL t`ոk. . ஻X_56ٶlȳhQ=qq5pfUM6Lܹ/FRRDFF?Ddd$R<r=ʜ_4Q)k`1K_Vy~Rp| p5Lt٣MHi|X>{o=УG}DDZkgXT `OW]<(t)0w.0y,Qp !Ę7 ?V掃%D# 5f,"";(.mx8iXX%DWl,0q8޹ܱ0EDd2U)&pvu,P[kXȢ mF ""adRe/2rEE掅,j@jX|cXQ")-mVXaNkj qV5QQ V.#ԏX:< 1Qgd6GM""f`"O<nF$%%!,, aaaXhQ@qqw}HIIADDbcc1|pEMMM߷of̘nݺ!""mڴرcjժxD\A|PQDfw"$gC =]cԛ a˖-(wmHZ͛q5נ &&زe lق}ZjWƴiPQQEQblذ6l~)!OM'XAœj yO{QXI9}:Xpis!9$f`Y(h۶-Ə~eupףضm Q\\/3331}tTTT`Ĉؿ?QPP V\e˖%FKU!KA'hɓ޽掉l,9r$Μ9?O=nDJ xg֭[O>^=SW_(//GNW^h,\f,[ qeBQov?Ǩ75Xִp\x7SNERRR>wbbbro]+**BZZ`وwsJJJ7iDL.!$B" ȭ>~ܜqc0es{ӧW_}>9r$`ݺun6mڄ*(IIIHMMz"2ӕО" t6o,DD `ݻZ O^۳gk?Xd_aa/~ޖX指l,;Iҥ~;w۶m-[6x'N4kdO}e9eV9ҟb45}䱿jk*_555zR[ 8~΁XgΈ,{aֿ? Q`(`&Q|I*22ג%K(Jݗ/>!'<J>4Xs#<#>\BgXϴi-*gJ۶3Xϒ%K|$kayb4nJZcuTVVz= 6xW)@8[oM4;2˒sa>Kf`޽~P=oOx3{̟?ڗA,cvZw}z'k]nqqq ue-@EȺD-[͛gXqa:j%rsAD6(@׮ţD,/,""n0$\Bhs "?~yصkWS!iIf`90)d%Xj6u*a = dd9"""!l_~HLL|^`ӦMqƹ]9r$"""r|رcطoQ'OwQ ,q_N=>uʼqP1EDhXJرzDDD0/V±c]饗PRR0qnbcc1eQTTTO?4Qo͎v'Be`ig8dy %|KHPOP!, Gnn.Μ9\T\nn.Jd&9=PZZ'bD˗c3gϯ}/^VZɓ4ilQkkтC2K5eEDMEDD݊tףGdee5;믿vn͘8q"lrTWWƎ?QQQ^?ĴiP~*o\\Q[[ EQ0}tmPt IDATE\[Dq[FP% 9m8re!iذaދ ˗?ɓ'cǎ;еkW"&&Gʕ+^Q3i3Yngcjj<Ǭ 2})(Q<3XȝXt|;?2@bT/\y%ЫZXd~0tx!s={20r>Ϝ:tUU@|ې\g|j:$~IC۶7&: 9>g'5 "RS-eB3TֿJIqKMg| ,,5Y8X?挧8$.!$" Rm[q]B萵v69qPxn DErADDD a*N,la;;jHҼqMu\p0h:XDD "3Sb>Xd9L ,"j""Fc cqr ,O.#d-)~{;.""<^%",QȶeKsg?*%v)KV a@Zi"""c5H%-D% ^ˬ9jkVDSHF'3\g|pC~N>ؼQs,O"` <+;4Kj%ڶc3""0,^R"`Wţqq@|"""/"\.|;vVf`ǫ)J٢ bYs>HѲ>,O?ڷukׯ7{T : >""27Wsaa'2 \vh8yEDA\y%0anxȲ""2<81HMUe[5 Eۦa&2Rd/n^~$""` .p'M@Tp{o; @2K(许B{4 f,,""3̞ ,_.n7@׮}~{5 pTX ," H?iXȒ""2Cz(Tf_\{zX\B `. HDD5HQqAɢ@P3̚Sf`B}5Y8Хh"sY8D*`.v>bAodQ[w\:sX)ED EVYPS\r##"" `䮂̙&&ʀjq%Fփ<~\ڸe0ED r\fYB'60K͘S|0,,dK_|ug#e٢^ Ωp>,""+/.w濿 `<#,7aoEDX۶w[2EDD`H> |8NIƍk>FA3XD|ho7o,DDd) `iNq}}l^ĝ;:XD%c8r""2Νq,HJ X ,B"Mf,,""#O۷ŠB ,cfXE s3""A +nٳq~y9s%1z>_sH-Z-U̝s,O"`(r VB}""]L\vzw CDDQ2o&?~掃Of`BQv?Qcr3hkx4̘S ֎g|r ,&2`XSg|XDDFf`7TTA 5XDd2oDDd `EbcM2޽@M Ю:%Dی4"VEqʏADi7f`!ڵ3""2] @D~kB, p"B"2gȏ>v 3&""2MhQ(\|U9eK_F'3\g|6,.֯8N$ X! J>-j_oh(""Ct(Nu?nxT `Ap`.DG; ,"Uǎ% (ΛgxT `Q\.C/K|Y,9,p׏QXkp>eK;x )ZI p@v: >""Sn.!uh{2w<\BHD,b)aV9!""1EDlGmd`I~8޵a ⋢1~,DDd t8@aݳ1Hy9{hrYK0DvƏLdm2U̷n mS3t1wL<"" ;""2 XDDz7o+)mf&p8fs0ED8bc9!""1ED R!ؗ6huÃs*X|b7t҇Q,}5Y8AҢп?0fy˥԰pNId `IB|嗢Idk` u?'_~6~LDDd$ @slX &DW%Ddo\Nw/|K "s\fd}"z֪gK_F?G_sDG6>^iGZ #OapNId""ٳ@Q85@y}3҇%DdTի ^dU|<cX &3tACom""2r>9XDڎK sA1,%dMX~8df\*FDdk `Qh۹SCJJo9 }% RRTT_|s{ 7F"" :(edk7®]癁Ղ[8 3T =zN}TCq-""b(COZhGesf]6 ь;Oxf[Ωp>," m,=fNhC8KRAP5 ,"2vdoC9sg}&")(tGZ6ljj^hy'vmH.!dwg,$"t( ''ljkŒ4`Ӧ9}Zdc0ED r9uמݻݗ fd۷ܸQԚ3G,j `ό(3Qǎ X˦&q_տߡCSg|XDvm۪Ӂ~Nl\rsqg`=ƒ,"2^-y ֭6"Д<86M`? \. :Ydpig6"3~ sA-i:u-Z11VFRSff`9"s0bhu~;woqѾz`uoH9vkkEp, hsAaYNmCE;aEDdK `QyMQwoE;d{ccrcG`"q^=7|8p ~\@v6P]~>&F,]js*2QXgk.@r'ЭZ_OJǫes,O"`oZ嗁#Gx2+.O"rߚ5@fX@X(iɴn.8cO)9ə,DeK`z];`QC("",ΞFK^ 8- ^|Q,3w>cSOy&wA CKȒ{NdZ͚%6 ֎.&XD W qΝL_X_mۊ*eMyDn_T7wABKA4/~!Za7=&,i: >""kp3wnW$XR_A5_p_~ L$mǣfB"YE:ȈHW `}h.>_XrMV0zk ,cl-:[/^,e/N @.⸉iLPaEwHVVw8 p8y~Kvd""ۓ._dd,,"<ѶkxgΨE{ڴާ];۞ K/8NO S%\ED'X؝0=wK""2XDd]eejvXq|loD~hWϓgΝ@Nz;D2JKE\=G_N] ۶rey]%N%Dd{޿,"" c(9X􄇋`SS3JK3ıt=Xq8v@V sZR"ݻ={2_ ,.{HNiTQ.!ԏi Χ]pAsZΩp>,".AEizSlzk`9$K_=2 +;[LJN:wnd#\BHD= 5,x7׺u|>ƾ}0c t hӦ ƎUVihX@3E+ѣQkkch!c2qh~u:~="Z.!t6.!$" n Xڵ ca+**V^!C7ĉ'lذӧOǭ ܂Bis+ZjNrݻĉ>.zQQQ#F`GAA,XXr%-[fCnўhe`僞:tkքD, Qo?޹S2KZF}uDDd `,Xrt 1z… 1k,eːoP)x֫X.!ܱClױ.9SqcH#oワPMnbVhB"rŇUgXUU_=*""V+**BZZ`وf3w\@II Vs&SN3~hu,-mcNJvheex``u@Rmۊr`& qjy1):8(YDDd*&''FVЪU+$''nƍ߽{w|>O+@36-k'R.E Ob[Z l3վz/ZoX\>:Xe ̸qI #8px5GHDr2߿2U=+V`̘19s&jjj?>Ѷm[lvp FNVRry`56k~r \ Ɔ{'];DEm9\Be` Kd|~86ӡcڵ"r: >΋ڵ+y޽eeeAii)k? p_@@jŁΩH#rsbȑf`ɀ4o^)BgW#" X-6mD[P P%DX:X2u$?7&"|IC?=[wNQ >}&O xqAkɒ%P >}BTVDd`8gǑ'z^FY`˖^ƹP,ܿ>jxCS?e}7sYc<>>A#XkBco08fad1 ⣏>sxz!z{*++~͟?.FuCsupp=MV++{yE8oΘY]QQbmD =}\.֮uaLJK]xA 7W$i3ĿeVAHCݿg~v'k9jح>m~>sewɓo}/?s>Z}̡g>c1"RRR\#Gԝڵ+ ??>/keZX]H'UU_czUX>ܽ(-g| XTz}{୷2Eطx}`n_$J|Ht$%?n.!$"NJKݻsc "<<{Lr>B\M]v?S!QPl*j\kQd_ei.]3>mV˖Vt8m ,\kֈ`ԗ_ycYa>ݺA r>q'"G~6pz{vH `C̹_={;?rHDDDrO?zcǎa߾}q?X mk׊vܸ[VkX=^[KUx.۰<3owiL(?A'?Ebhg`i7[ֱb"X'"GF:u ׭3o\DD!rHkbʔ)˗H~u70b i\>(ʹS/Wh7nT mͳ Ѭ}}wq^^XTv L""G8HH+_j'NDD,G8z( ^{ YYYukkkyf\}Xz5F޽xbj 'OĤIv),))ŋʹܹsF"CQy8@nm 55[ `{/p.A"dkS|3K5v,f`""G O_~FDDia(8n݊["##gƠ( f̘?۫W/\ӦMæMp#..Ũ(>}:rE;Hl QzhJN)B9jJ=KhV~A{XVM m_&冏7^ϰnj$p>ml${p=|s>oBAAZlT̜9_} /ɓ'cǎ;еkW"&&Gʕ+oSQHfﻏɽ{{dVM^k`5>fd`ڬ} a6@ ;е:BML~GEs'jc"" ar(w~f=N>}2J IDATiTDM]VUzN.زEd0{oNm!"{!/O]B8dHid/\BHD׵k_r:Ǐ=t%"23AU#H;w?Jv99pu `s a2Μ}]jO>.-u+.XXd.3>p ~Y86'w?p#O_|Y`lG(}0EDְvh/XYųRxFgW_Xze`@ZZ^- 8zx53׵; XO43BQȊ^/Y|zoO>1g\DDYC Q@U+#=H "rǎ=:""a̗ p\xɻ ,3Qo{֗j'Ce_۾]/Js}k`|^ZZ\>u}<6QPDDBII"yJ^`,"2LC=:|)SDv8p{]Dk.?+V*kH&kJ-|DĽՊڡCvEg݁u뀄`Fu_*9C `UW7?;wdÇEV_񉈂Cű61ED R^#shG ȑ@\鿿^m\B(wޖz+^YY?RPUI p"JfW~bN۶UkkjDs`ƏO{ÇKE!7df'KEjdwm/RS';Kdݫ? .p!1Q](i}A:zT@Ɍ+@e&8Ѥ!ږI|:9E9;;I|XDd<5}s3ձrTV#k`#ڟ~;i&1o}^{M,o~,\X?lXOX¾}mli桞YDDwx$B C R) `A@ ~A@,f* B!q-e|?ϳNٜλym+9`C diW+=SzD@77yQv_k.~jX iVp옚ZWZhU=7n_79С@{ȑ@˖3reaء˗m~RӣGz/[ED.n]~}㲜!">B֩.T23Տꮧe;R_mڨ3""gOa@""r,"*Ye`_]\}tbW]Wb\ƒ ,@eniu:ie`9^g[6̑VS۞Dk2 #GT@^unq萊o\rDDT0ED9/Zp5 `k׀Dի40PMOS(Ih񇱨CƚZ6!cPAYkSdq/== -/mMF fv}y `T{R`{=N40hNu!_W^)<{`J?ijy7X -MeQ%%VMS+i}^t40u%%pJMU˴,͍9b?z>j\vGe@9lm>Le`իx"RW/COLT}j""r,"*YZjռB,zII'˴i)e{ʶS%$p/^Կ^^tDMA@%fͺf۶lQt颯RE[uk}9Q0l?TlQ1ED9<Xť9og̀/UF&6VTY\MXZ`Ԧw΍\CGajX~9~ym6͞=*1!$hʸN4kg jO*lϲ'6m@e`N=JTz0ED%'+K/d.T+.N?~\M۵S]L&-6Է;y8Ϗ}oeK 뫬I#2~TWQ1ED%϶T];y矫Tky-Z5Ujp=J}tv[Ϻ;aRGв6iLqʲVOq^Sz/m?XDT̝ ,X"Nj_r,"*9[}"V׮jz];7'._23TX.9C{>#Vd?aPPQܞ=*86mRuҀ7$$M/Vk,"*u6&NfPgL)s""*"jo-%Pq]Jj흿c; ԬM:;nXjZ&0~GuʎeNog݅0,L.X R՛0};uR5^ջ&&P5jr|ʕU`X,ND+  /P7ogϖy2 `QHIY׈ VJ5-ֱclW\z8Bh]@ݺ+_HJ:sz `_?੧2g,. ,Zd.(H mi…кSlZp}Vp2Jgk| н;0aBɞQ)HQݸQ]ۘn*@(RZ.yZ VcU -]EmP=S_V~ m6*X@ `Q6{>rrϞjnADDbW}HLHk@5rQ.4Zw8Ү4ZաCj-{Z˺69'ӧU}p=e+3Eх0G!tw/9mR<{Vb !QMK@VVɞ Q)/-{wo(ㅵ#;݅P˼-3Je9d`;)U_230 be,"*^>x7og-.U6T9e`1EDYd`iܘEDe b/ݛar$(޽jwo\ ܼArҗyy-Z޺r{Rޱ=˞BmSCQs/!| `QV vm̙\5R5ߋ]6;drS* e)<x==+ YTN|NEܵ?mv,dva}(,"*G?""XDT|(Q:/Ξ>RQׯ/)͙~j\~0m2[*TPU,BBwO77; We9u!TIMύ8i+͍y>0EDeƍ@jjɝ cr$d@isԩ*A+*J V=Űmo]/')Æy-Hv.f_|xv2W^xpηbE 8ʾTpV]JBN,""MaS `Ef&jz ˺uji௿|6X]|l]SЂQII@B|vয়Th}yJ 杍bݢW/s G={/ s\qp <:ŞDTh_W(""8sxac'N QWZe4 (})}Iu>cǪC<|uΧiNֽAMU՚_sSGeCըa{ aa ׭Ήyyzm7+#HN. -5y΅1ET%'ZمUڵ|o_K`Ro_mXR:efKȀ/\`߲e*qY?II.}];}w8>?"eJ`{XmS]7ul8C>H-4IϪH˯`Q]#muﮦ*c&uH}xq *mJACUUۓYzNJR)Tl%*="*碣U&>^=uf`mܨHNi4Q}<mJegx>_>_VyUۓYI 2U%*="*4nGGOe fe`ժ"DqMZܫF Iuk-]tzRWϜQt`S;~\=3or@.7WZLbB:e.ڀ "2KFC `s%u:+|3Q9}QGX*WsZ׿ݻ€#ZާC߸/ׂa{!jVїkf̀6mСzwpE-!ԅui"*ٓ6/؅<-XAD"*l3}VM+dF 3SD70ϲ];@XnUX[$iwϝ3_f Y#^^j@4lYM\B8SED-ʱի#EkaBkq~:Y=Q_vn  ] ^~v.}3f5< `eK}ʕڨ5j,Z( Rv _ޏ ,"*ED-ʱNF G]ݏg"Td(8+zKo ο6cǀq#^*PfX8^=sm~w(Md$p^ +<\qc}=Tx@e]ռyQV ܞZVgXдzHy*Iy,{MYD=JTz0ETNI oLվRQcPÄU^ݬX~Dܐo:ѣgy> an=cUCPW>_?ha .љ1C}PՍY[Z7\HQi]ykV5㬻3cQ"*磣ɓ|cu*±{]t#_ l$t*jԳz% 00A Hi?LM{GSPת_*P~_̚ űnCe`V?h֠zbpƍzV*VDW%U `ժ9hח|~_ `5jOn-.,SB9wn [`-0WoOgSdm"%Q҃,rjF}^+ @UCP;v Zb7:^;*mBYa2ՁO>~C}v#$2bE5>@|`NU%K^+s%"*9oBDeͮ]*qJ eF_YB ,0Ѱ~8 mV.q~ !AkǮZU !d*<]ׇnn<nAA*1!>]φ~KOחU ?Hj}oou+;/u?Q9uS i_& TEu+U1b@ppN&}\ ϝ3U 3߂kw&4GM"*XׯgۗY:-[|.6V_f嗅nn #HӼHDdCHVB&>prM;>¾}T;;>?Զʚ1c.[Wռ}a@v!s]ӧ˖;)Q 4nj#飖] X ~;u gmj*Vϩ$TL*TxZ1E8\[|<de:U5jtIǑ8z:Xr2Y27 Hص ݻ/Pj4U 걚7)'KN !,CK{>0n~?4rE]I֗= gSmoкue!ef@VAa Yw,f(#ʉŋq TiS+V]fT6ֺua5WV5o1[hf/Q! 2ޏQ=x9kӦx]ݬq.nYY*UeyrEKM5v'PͭдyOOv$Qip0ТE?;vT)Sn_0a;YYzkN:ۤ . /V3XnTM>!T yDS7Ձ_W3EDDkPb>:,ʅ2ҿʥqr*R#11N9v~{I[ӧ&CPzbnUz<2 ,f`QQBn]_Ti_*x (zgߓSRTO?U5O/CDDT.i7J>o&~={& M6?gyykobm:,FBDhy7|!0E)<ύ8LEzD@0xۅcPT޽G+VTWYO[/oC#1l};1Ǹ3Ϩ*_~5Qq2TZ@|J T4118߅HN~ GwݥbwVw`Sg@j:w6+URSp'"WҤüARR |mݪzkid9T8U>ж-&""S{$$(EeXdqiQQ*f|rl޼!dLupb2R 4d'g:FԎ??/lW? 4^dϧ>c0va_7u*5Jemެ'$N~%`V@>dXv4tJhK6mC1Ax~!!ڴQA`T֧h\܋N*ծ-[ڜԒ%juk ٌ3fl6O[}ʩMta{&!wp__[fgz{QQo//#mڨU\,{ئe L:Z>#reHJ&5k&FrRJ)ʕ+e@@B޽{x,~jI9c]JpǎI Ȕwɧ2:Z->a)UIH t8-)p)y JR23l S.3,PSww)oޔrW^?D)wȐiS): 㼋 7~Sߗ+e|m%̔oo ;(Ϲ*32tL-WoOyejjOO5'.R|sHOOdzJypTTӦ؞eK~33S}#5R='Rv"S}$eVV= a-lOQڵ@1\RĖ%)BH$wm~ŊR!rÆ "eBfkI ]X!|=)wWy.O6n۰|s4 Z1`\Vik7Y:d}:˗lNʥK7{Yʕ+6kHhFEImÿ֭U*䒠 -Ȓ]6 H٪;Osޕ_͍umH:<UHߢZi2-þ!W=%)u3^4j"l9{6-[؞eKQgl=)8XB}rϲmZ=]D˖ t>,I)СB=z8ݦ^zR!)'M;TSV+w&y?;r)M&)oyRmøE2+Kʹ ?,[^3tkO2)IJ??nv)-SGʧ24TebiFV/ӥ|濙r?ZʉxG` ˆ@l2T=8v ~I HDD功js]w# s.R>h&$ycpqH)!@-nz*Yz<4m/ts""~˰mPI Z h:{SR͏^<ի[ 7l9^؄H7?ZhȄ9sTܵk76x)L #>[OMO =]?anYO'zYzlӫ*t)so4vm૯Wmiл7qj>PW&k@+x_gƌ6U ~xER T9~>]PTmP%{8 !!::2_#Wn|rF# t؆E;tVt٠kJ[8j0__< -놡BLU 0lZAEy֮Uiデ7*[vn, FP`nk_.]Q ؊ :cշg!M"a}<hu^ <yDhq. `kW8U:*ԫz*Cۧ@S1سG 05DHDDdM z9nƍӧؾ};:wp;oDDDDDDTB(ؗ\X *XSRRngz"""""""X0^(ܭkeYòtM"""""""*L"4mBH)qiTDDDDDDDD,B о}{op)%֯_޽{X1b ""իWBX%"""""""* `j֬߿?~w@VVV^1cz쉞={Q9#$+nmgΜA=p%ǦMX3ȢA8pMMl6CVZa޼yؽ{U||<^|E4k ^^^A6m#55 e'.._~%f͚^^^ ã>~!c]a2,=]1{lopwwGʕѵkWHHHp]C͚5-mR^= :۶mv_gu~W̙3Cڵ-Cg͚cDGGc„ _><<2,yy)zK.W\;{};}t(+{Vrr_ڵ16-|m˗[>Sur_v=K/@>c?ݻwۭ_bC~Æ %pdk˖-ٮꩧ,mf}%%ۻ4Xp%9slXlOדlɸyg/tA !d-ʒM6B9x`:gsMSv\]LM6M ![?o!жm[>|DJ鉰0 4ٞXoі5j8ݮz˗/9Q$$$_t 6c{hZ6l._ ///,ZȰƍ7ob̙SNa{x9soIIICdd$ϰ`lOו$CD$&&B4ƍ}؞KAKn_l뒓{ v5effbȑ0x%F5IDAT}٦E$M6!)) 8ru)%^y|ٞXDx駱vZ!h")Q.,[ ZnmD0zjnnn5kow ;w.fs+ξ}Ю];|ر#lقd$''#"":tw}va%}DL <<==|rTVOri8x ~a 0O ;~7tݲiӦ_P~};? `QYL8zm5g}~!X`Fa `ĉpwwǧ~ )ٞۢE tn!~i-5؞iHJJB6mqFtF׮]qFn=ze?gRz=ۺf 2?#<<<|r|cǎaalSbӧƍ>|W^u?۳a-,,2t;~:5R!===U8}=PM3|p)uu ۴x=K))))~R!;wlYz1E.7o.2,,Ln޼YJZJH!ݻw )i{9… /ۻt1cF,k۷BȐf)… %K =]BSNY֝8qB9{lþlϒqu+]&ccce͚5B>e2))ɰ_\\ Bټysw^)iiirѢES !رc>ӧBȮ]ӧOK)LJJfͲNΝ[2&?홙) d8o}=s}fgذa9ئE#홙)NKm޼#бc+]FDD=.۳b ӲVZ/Wre˖266OMf{{뭷.=r `ItE ,+!dhIkn֞C q,~گ9݆n]dppeaߣGy-?H+VnnnСCIiϭ[Z{zzf=jժrʕr QgrmZ Ҟ.]M41|FVX>sl҉54h`ڴihڴ)f3hժ͛ݻwKqlov`{¦#lO-[`ѢEԩ0 <;w+p_kqss7|kעU222ZjaXn-[,~"Z9Iر#:ǣ^zHKK:v숏>6l׿cذa CJJ *TnݺaXtiQ>2)?)웙W"55c@=1smZ Ҟaaaػw/֭͛[C4ԭ[F¾}0j(,K f`Kc\XDDDDDDDD"""""""""4ȥ1EDDDDDDDD.,""""""""ri `Kcdd֭[KTa20bĈ>R_"""Q97sLźdʕ+s4sE ! (S.`̙xwKTJ+s/Qy|ٮ|2֯_oqhܸ1|}} xWp’>""""%>"""r !!!u6mڄhԨQvK,AVVԩH;~xih7f"""" ,|WN2^<'VȤ%} DDDDG `vڱcN>k׮YHCh99n6k,c_F֭QbEx{{^z=z4;0i$4o~~~pwwG`` Zj1c`?Q{1222|x|xаaCkƐ!Cgj5^ҥKѩS'wy'>Cdee9=ѯ_?^^^^:^̜9KLLѩS' 5ksCJ*o߾?s#""b'\1cBȺuJ)_Bm۶m;rH)s̑[lBi2 mݺհu? BYBYbE)BYbEyA~嗆u1ϭjժ200P !VoqF&IJ*I$K.Y8 ,*UH___>|w&%%E0r YrecY֩S'oaäB1nЎ"-d2{9ivM>ӖmJwLKK֬YmL& !!!}m_CRJ~fOH777˱͛ϟ?/k׮mrN_DDDT2EDDDZ_|aXUV Ç/pW<)%MFbb"nܸ;w",, 7oĄ  \b)^V-\|p.\Y 8"%%wƴi`ʕ+ӧ1tP>|IIIϟ777 f 8/^Dpp0V^dѣԩF M\J Јf`]tɒmd/B٧O)Bf͚T}'QQQuRJٽ{w)p#<BȾ}H$ߟcŶmۤB֨QC9&**JVPA ! _)G[g5l0O>ݒte+WJ!lٲeG7nm,X UVcL&헒"4hm?f`AXXzꅤ$Zʲ\+ޞL2^^^v~xxxPP-22ĉnO6ol/_.sGFppmjԨnݺPu <عsmxW{LYƲp \RSS|r!,ahms!\zղou]]|||[>"""r=#F _`Ĉ8s v؁`۷PC:8\ʕ+N x%-P:u8l6p5T\c4hƎGyڵOi…㏝nwM{^^;w-BDDΞ=D.tQQQNY&իp?Nر{?.^vaܸqٳ'5j4xw^KSN9>/)%.\*Uo@ݝ:"""* `G}ob8>A$D4El#ư Ür6ci(Nm k_:7/tL[M4c 9Ε1%O@NOg#pM|l\x>^|cWEEEaСQ 2=(gTww^1F@={ҢZڵKvSnnϟedd hMjkkr!) SRR(cnܸ۷oGc}^ùߞ~8b5665RRR4sL-\P-r v8!Xg\ƺqŋZZ2Ƹ9;Oww4fw?zmܸQyyyJHHɓ'UZZ &GwMy5uuu*׵|rA͞=[uuu͛7ҢK.%}Ν&UVVjٲez֦-]Ta?"??_[?x4@DNXU^^/jɚ2ec^vf9AF$nzk5Jt5utt x_ϟ{;'I{~ǁ.5ÒN=zg̘!Iz͒'*111b~_\p!b[[[[^9WFF6liySEkjjbuSJjkk_{^"QiZ~r$I999B#i/O?:~ذaBo&++}͛,4g8r*..y{*))$566Y]n߾aÇ$߿?ߊWaaSs+Y9ڭdIEsK-Z$I:zzڹs{O@Tvܩ;v(--q/GR3dժUtqɓ3g._uɓ%~wQٳG:^|E>|}:fw^͞=)$v"eee?TCCCX(t97LNN̙3}w}W֭SSS٩Ǐkƍ;vlKRAA|>/X ߪ@1:t***f]~]RhնmTZZ9 )I۷o׼yo߾c{wQuu$Iֶm۔k׮饗^Ҿ}_zUUUUZ`Ga uAuwwKΞ=yNXekƍqc1?Յ}8Gff5؊^m vذa<)))';b[]]5̙㶧LivԨQn߸8f ~f%%%g}hb>{Arʰ>K.]l5+VwӦMasxkcǎr?fͲWv~YPP`;;;nݺ5C O>4h٤I˗{]ٳ6;;;SSSÞs{]p33x``~,14msڣщ'okȑ *))o36Zvܬ^_~e?^󕚚VgҥK~cz)UUUiժU6m~Z[[eѤI{况QofI||K;vL˗/WVVPzzf͚-[ac=Ϗ>H6mޮשSٯRӧOW0Tbbrss{n9rD aW\ݻw뭷Rvvt- >\tԩߟy>}Z_| 4b &h…ꫯ"Aqܸqjhhкu4~xIw7>c-00[nUii^}U ;iX4,xBQ;<X4,x< FO#`iX4,x< FO#`iX4,x< FO#`iX4,x< FOg*KfQ%IENDB`tsung-1.8.0/docs/_build/0000755000201100017670000000000014377757020014547 5ustar nniclausdreamtsung-1.8.0/docs/_build/latex/0000755000201100017670000000000014377757020015664 5ustar nniclausdreamtsung-1.8.0/docs/_build/latex/Tsung.pdf0000644000201100017670000260761214377757020017475 0ustar nniclausdream%PDF-1.5 % 1 0 obj << /Length 843 /Filter /FlateDecode >> stream xmUMo0WxNWH Z&T~3ڮzy87?nkNehܤ=77U\;?:׺v==onU;O^uu#½O ۍ=٘a?kLy6F/7}̽][H<Sicݾk^90jYVH^v}0<rL ͯ_/CkBnyWTHkuqö{s\녚"p]ϞќKյ u/A )`JbD>`2$`TY'`(ZqBJŌ )Ǩ%553<,(hlwB60aG+LgıcW c rn q9Mܗ8% CMq.5ShrAI皎\Sȩ ]8 `Y7ь1Oyezl,d mYĸSSJf-1i:C&e c4R$D& &+übLaj by+bYBg YJYYr֟bx(rGT̛`F+٭L ,C9?d+͊11ӊĊ׊T_~+Cg!o!_??/?㫄Y ?^B\jUP{xᇻL^U}9pQq0O}c}3tȢ}Ə!VOu˷ endstream endobj 3 0 obj << /Type /ObjStm /N 100 /First 831 /Length 1432 /Filter /FlateDecode >> stream xYmOG_CUmҴ%}l qH}qۻ;GX {ٝyfٝpB YH R(K#UBZaaX{)-QB9=F{Q+u53^ ҡ #t_aƂ/#{a8 $:}/pAR(oPei/W(o /qO D0E/`^ }Qb K.Jc)n7FF d-IbJyC9Xa.=@S8 E#W(cQ ցtcTI$aܒe\ 0 H+H%^!S))% (f ,$%,VA&1jsJ0f]e|fYoyALxsf8())H!s(NSxA[ bF 3r}8se~-/xu=|xrOy3zuß(73$$II,I\)u,l_EbB⒵]`EG~dx7$~353GԶף'6oiG] '}n: ]V[%?<Ρzx/ ޒbږzbwGpН;67QpWMsfɺm̚Y-#$I#kbQUm)U8#~G]vgtXp/B>䰞2GCx2Ytw]M&i-ywKbOxŘHni:EΔ)Gm¨Z3> .^6FN5q{ozC_0YٶLf!Ս,^17}l_;Y/vOusYC!kc&[& Q =\a$eYN{7-x}bs};Z ω~KY`j`M۶}͆=eM>2VI7A*=1.n :xCv[qj8c"yr5VXn D}UZY ᄣ)n3f VA%nӣ~6;R:RKF1I˔nZc&QY縶E{m|4}=I8Xׅ:5j2[r+>oέ&trV}qS&%@y \V;OtݳxP:g:6Mk瓫n|]ܷB_u}6sqL |eCGGli@DaRMQGz4ܨ6c"O~Lݩ_2ujQw_X{_ endstream endobj 204 0 obj << /Type /ObjStm /N 100 /First 881 /Length 1436 /Filter /FlateDecode >> stream xXMo7W &EMM{ŲDM,#oPrr qW囙p4B -0(1B+%wja +-IXw6h\p2MYA<Exn!E6N$W@4I W^crXX[)dĀ}#v)DGZDdKM™ѐr2vVA܌’pSXz@?kHaiQP:` uL ` )(h#m4.#t0P&lM pMNj51P ZcPَM9 (:V $n%ڄdX{]#G@ ~.]$d(" w^RP&O3q|)^'t5ZϗĆxlN.oPfw啕VU~^SaYR3x茚knrgsîv'q͡[>q{>5&P̛Ix}Ywb/Yg%2ŠyTn/ng9c]f {_5]ܸdf _6r^rj="er p=cHz/9akAs׽rxY)L|]7ZJ~e渜/3N<;VOmKN%we%r͋N8/v7a/U~ß~4NyɼK7rę,L)u nGˑbro{\f/%FsmuOe9f jnVj.فzWWYޏ_cZ|Դۚ1j{·S֯v֩v6 0Ƿb\*4>I)\Je෇p\V䜖 #o[ r\1 nhնB5g cgoa9zAc5v.|QNپ%w>I{W^3o{hnsUYirtW6HzqOMYlvZY;v>l{ҴfŗGeMЮ3q y3n NGe;c:}z__ endstream endobj 405 0 obj << /Type /ObjStm /N 100 /First 881 /Length 1730 /Filter /FlateDecode >> stream xڽYr6}WoIcNI&7i;męi"˲$,vBg/g5׌3KJTLpɴLh<Ik`-Ӵ|aV/+L+ Z+KUUڂ 7@ fBbEdB вQ@Mű(\X2p@6@x-d dl\t +4t%c$<\1`RHL.9.K `JOtip,T  Ț+JiU$-2t1` xrb8+Հ1YY\1g(+!Xx_)Ā'eĂ54Ч <6O@*KAըx,'_sb4& "b\7bP0(0`!EX"0D%![!W>DkwZ]`ؼ 4r0h9``p8r0h VA[M.,.`.0Xl`Z0X.4m9y7'zO'窰8F /i8kNg4ipatP@n-y) F KvE"a4,zs-~祸ZS+~ax,Mwxe% F̙pV{< ܎vlqĹapmd^۽5'>x|C}ުe{Za8Ru㭛0u]tb&1_.PF21ү.4Àm |Gu8,Ȱ;wLC60q 0 {c; [-Pͺ_ AS)6퇶_\Z!ʻu9fDVBϜ(|I Wl(#Ex"3 ~xc*KJz+Rl_9GtY5/e}oWAafۥ+̩uH5;ʔ-;mfއ_˓2f?ߦ\rMCN1ۨyzN [ez㯼}Bd?܋KՔ v_ ;e4|pi$dJf2Rx\!*sJ:kZG6 X͜Z:(PAxfi*%h٧YO)g!]g3ou <zm>uZԎ1O\)Xpe_ ͟'ξNp¬#ut@B-1XG77NOQN}$!ppz;RzZg/z]Z$Bo^9k./Jtn|٦EWܿKb64݈& UR`f aw3CLlD|`pH7aJ%JH:Oebѡ76\ R$y`J壆AnX ~tNњ]| 9$>v(LU[9?R!݆M{boE>᷏8'L cDƷN .&ȸWh endstream endobj 688 0 obj << /Length 586 /Filter /FlateDecode >> stream xmTˎ0+$$0  a#A%߯jD岻fc;Z̫MfG} q]/ޭmޯo⣩0Z^x]fkn{E+{*ʧypg6;5PVpH8$hmڢ*߄zR:")󨺠3qXysO'H)-"}[˺s 3 4{pYdrK+ a }ѫW{ Fvm7344AGc ڤ_86 endstream endobj 690 0 obj << /Length 239 /Filter /FlateDecode >> stream xej0EYPOgFGޖ> B[BqCZ+ǡfuaιB x2/? $`PeBg+hz|u]y);x7W`n^'$oYe<Bgc3p q]$Q r"bǨ @s]v0z )zg`.W،]V4]8O3ɂCy)h9~(zWV+ endstream endobj 697 0 obj << /Length 19 /Filter /FlateDecode >> stream x3PHW0Pp2Ac( endstream endobj 737 0 obj << /Length 1189 /Filter /FlateDecode >> stream xKsH9JyKsڲ;YWCl "+Daq@Y264{zwXnOQPAP<(8{X @y2篏2waG|*iA.t;9@xcF@,"<BϬ]Ĝ`/I+[, BiAIQ'+%,A(*.{LoCgl X? 7TvG=Oc[֜5HOsC'>޹:?NIv"s_] }!MXHk!yvQF}蛋3Tn'sI&SksYwrR.Qɟk\1g:yֆהm%Ye΄i!eVd!c[Q6C&z|z9vs?Cnm?yt{G\\j<,@c%KWFZ\e\}Ftx|ym'n~cj^s}& FŎ)ۓ JWNkEm^᫰Cg"Kn 1}KuX?4HP4i:Va jm4 kno&iq,L15IlFy@$d8JX5*9!תj8ɢS*u|JoV*HFOlgdՉch!AL&]9dBd$1 !y&l&01ykgY Wr5VZIx`V[4pQA-"h-ܾt"i-c2"~ʡPe&S2uTXkNj&3){ELCL6TuåKeߺZ+lMLRaI>7Fx~է#gILZzܩjȏ\'Oqp1Y٨hSuT5oỶ^eU ;_.N3i1GV *UHn͙3IZFuT:@7mjSyg~_@H8Jbڟwk r{v >z?@0_Ec@$Rpӹ2oF|@UTbt endstream endobj 791 0 obj << /Length 1651 /Filter /FlateDecode >> stream x[s8+՚ l'&vmxf/Nl3n7ip&c~'}XS X{yw,`F+\_;G{o"O,u|B,0Ƒi:BL]U^Cq6^j}RF=<7uu!Bl!t|JDC}?L2sjFښA2γԘ# NyU]@Rv9KKe-)8DŽagPz`c0و,ڌoZ3jN{gI?Q !!|J@ }Z0ⲊR 2MyTSEՂLWU@-T}})jCB|z2{Ȇt'ڙOl"\ Ҡ2<8Ľ~a63"RV&Nu)WAek\݇ru:$/e{=Qɲ$[vÓVws?ʘYtBέHw^oK}f. =F`e;\,?˳Df[¨WnRk)~;qq"|@%b 9J1kvAkA1U}@SyA{1=9rA4Tjk4ڡ'P{Cq>̤Yb2CٴcO76DB8QGh^buh~Eqۏg9u:pxbM`A! ӽO_o|ϺJ-[uQ"xS$2 h)w endstream endobj 606 0 obj << /Type /ObjStm /N 100 /First 886 /Length 2302 /Filter /FlateDecode >> stream xڵZQsܸ ~_ǻΔK@x2KfzL%yp콜'77ܿ8a%JK}AI4sPJܦXBN~]CBY5\r ~V FEB4@ʎVXA h BI@4\1ՂA@RD; Ȗ@JR342.Y(Q9VAJ1$vmX:?І>՟jxatnࢭ E6إʠ ,Q8䆑Z0JnhPȀ%Qةx ЀR?C?Hh+# BalŠǡ>(A3(K Rfp i0j2A-^W6+5AV\p|bSCQWѯf:sW#ZN,Kى5en!a-_0gb++pU~GB+s357 w Қ{fJ d`swq`?AHJQX?n_lqrs;۞GH߇V߽ʦ٫a;^Q:SxQg~8>ó᧱߃I~y~G~?uS?8R_NҐiL=]>;˳E^1ݛr>zš<.oR@7zlG73|o _Ĩi鴜÷QE(v{mLgDy_fCOw|x84}Se{4Δ7"nnON~v#aCC[cS?NgoU*O?{3{MX\M;/HYX?9۽v6af~=mw%\m./N6C-2iszvSx}|1ͯWx :b˯Xk߿b}N2{M t\?؞ˆq003/O|G"v| C}*^]s?~~~~QǣG:usj0U0ɋj4qux L5PMԑslIs"Kh(}= ֖b-e,#=E3L+iV##1H 3ͩÖQ2|78 B*ٜ>< <+Wx+ϩ-OC1 Qa<$xW EB,sD99i f(g5|@HnɎX,AY>ۂ4QA3&6B=:.9'E cgŴ6-#˗9jdW +e 8UaJ,Zfx,m`Q_1Fɂsj3> stream xe;@SJZ%ҩ*QC"&h|4|W'9L㖌?q[ET(#40 -2ͳG iD—$ ^:K{U76ՀR丏G=Op8c6TB`5)'W7j endstream endobj 800 0 obj << /Length 113 /Filter /FlateDecode >> stream x3PHW0Pp2@ Br.WtB PK@B(WH(sr9p)XY)01344RIQ,ӌ [+ endstream endobj 807 0 obj << /Length 766 /Filter /FlateDecode >> stream xڥmo0)b&kںEڤqwƐ$>;)"s8g(qBLD!%",b1qOIj3'e,tQ{t:/;:5(0ns4q8#" -Ac?G8Ba9v9;+H0 YR;WT])++K8tYK{qc;= v|WzV:*הձ[40c$3,bHA.$?UaF lIML s8QdI FI@cHV3ox3cJw<~SәfO^;^E횑.2"]٩2]h\K? ƃIMml}e]f3"7?ݧ$bXǗMJhsJ ޚ >U8Jldl Åk˾]Sɨk5XQѯ=Q1*C= s64O~묮0xK/#)Pz]6v}ֳrOo> stream xڍn1D{Ŕ!$Q $P("hG;0{RBPQ# iN&tiwf ! J>[ׇz}R$64Q~ͯv8`Ө励LEm5p>f=`5U:益NZAV$YgLg4czlWkHM endstream endobj 822 0 obj << /Length 2484 /Filter /FlateDecode >> stream xڵv8m"$f܉8/^+LH-vspi=T H3Nf`(TW{˳^x_$s#[믞[s,8oaݞ?eyrJVfq[;+|7|+"k/Ed9Yp~]$~0O\E QOΗc(q(Gs,b_!vqDˏ/W`A2 bE[ ,<~x}b-Dp%]ouyy+;g$]:Id}u $h6u( ~"#No^9g g //yμ rR^iGDjZw6q3!;BYX VzvCnh#TYlVU膓?aPxv`-PHq;+cvWNdkw5qs a-RW!Gx Wyw4tySD# ыͪ\iq}G{޽o ]S|9/Rm^6 Սeo07= z9$L侀EOTg9dDX=1yafh3;@pKz(2lJZh,aRI)tF1&lF >~q* dHP%&an&Y,?vZΕZ x4|-&g)N$Zu'Lv"w톴PäޙiT !;5;~5I;{Ѻe󍚤j*t'r]WP }~~XMlJ%p_ԅCj%27#$Eb${+Iq{$؋\/:}ӨUlO9baW^m9(i=~(a沑sf2e#x>Q8ּ1c(J)z*GtԱ1̱|hA *A0Co\BaoBm!mZ"No;_"8Ss>SԤv=,(oײ0% m@9mPɪ}"( ^몤ɋa]%boUՔ5w)8@-iї ^ˎ$oddAX^90/7/gn #r4!\uƇ$W\L"3ƻ@q "]T3>}x=yE,b*#,9t,&dHS&zS+Uݙ:w\ Ufv┲O#-U`ͼdl%B5GrZM>ȰMSj@jnu秧`xqհ  #N_:cBݹ}SCj38pB`[h8@Ǡtlo?:3u839G1 9bQ҄ W9ܱ =I!1-!K42Jf;7Hao4$&l4:86#GfEdץ1ȿ/^Bq:qBݞTiZݫD$v&5Ckj: D&lX!CWZ-ںo,'޳y!$LUbO-ؠ\ FB ED]Bh`>}nt&= Fϛ#6}ZwffH QJ7ec` proNSKIETbdfLE>!ouїƼpѶ=Â`aezz̈Fnd#1pT&ӟD"B0p}[[1=?n endstream endobj 828 0 obj << /Length 1474 /Filter /FlateDecode >> stream xڵX[w8~ϯ,v$[V(-Вm{(#/J;㑓85YJ/4efF0`|2=\#v G#tPD=c4ޛs+ͺ-n, #L\elҲxBK)Y+p'rajb>2p38m-FO` xqdvrCq5k´lW݀^d9 IkqDo֖#nRe]N2\ޑtTAFY8+E..ώPPw]3g|BUySPެ F u= ܉}ee{ns:[iֈ laĦ#q9 |tOxy:1w WM~:y{{:_)+Tf:BI% "T`;ez ϪR."8fl]mi!p|="n ylX዁豐Uh粱%&vc]䲩^!XV71P gvox{QXlͺZ?/w$Oq0dKCq;JeW `p[eMjD{؍0$\<oiޫl$T]SM{VPPmʖkTOjKHFJ ȾxbH ۾9Zۡda4MjEJdZt0W=DrG\K"mZI% TSl!EMUf]l>/P0wjA8wGa'++}~iF~D,F h9 F'ù~!7gcbAc*-==X endstream endobj 835 0 obj << /Length 2437 /Filter /FlateDecode >> stream xڵr8_" x/7 ?Gz"a,"Z I@R: cQ8u[8Mw= Un"^*tJuC@{~"\-nߵE3_.I@7"`$ u G'\o7pr3M Yl)mAKhG :n F:.KxJҭſAVmy"As=B,-9e$,"9m>L)Om;f:!rm$ܿz;:GyB{' ;đ:w(+eiDB UhmuAIHݾw"gdLnz I2k" )wVZS V08 b[i<ޚ^.Z Eb{)>Lܒ^:ymv8fia5w/=nN u)bLx{5&t[*sK2ˠuBA] 1VN/c|b;MҫOVַщiFQnVf,kDAV;&!,}zs1|e2)C\s u|ݑy&nrITc>A"f*ĔIH*?( b nF ;@8#As Jl0Fj ȋ~/а3ưu uE{Cmv%$61E { lmLtJ9 < jֵȈ\?vZdTeԦl*k4$CJ7mY4u1%[Fڧfکf87ݲq}eaJ5i Fiz(l9c^j"M .lj;, yf8uaoٽ S-JNzoTFu-1XoRC(f:FjYC~6%!Pil;{ɘE a$z׸jgc:n*I(ʜD - 6< -K#YD'?`pGF]m]e5DzW2& ҆Cj[ gWqiij@wmW?̍g?Uo!K,e{P3Ʈtښ`'i#2eY^I1<un}b𰇩y13-!rKbf'3;|;_|?s%ٽ ty{eq^>SY4#Jj&k*ېѵHmkBSA_:}OKo4N6ʼd?eA.9UF fBԯ&$5i)H`(65_YrH@€ a-%: endstream endobj 842 0 obj << /Length 1477 /Filter /FlateDecode >> stream xXrH}W U2e ;f1 'ٯz{UE3>}"=IDw.k?J]jGGWo'5M oA/hB⃦:*Q>7=}i4\Td ODDTu/娵dZ P GhD%!?^M{w9iG9,U "}.z4hCYQl(Qk> qX)%,[ ی,֯)OX,ߏ_5yaKQ'v(0:R^py SV{Ѩ守C{/9XYZ:FCԲ3uԧ o~۬C W\+8x ItHmӕ/$ @, endstream endobj 847 0 obj << /Length 1094 /Filter /FlateDecode >> stream xXn6Wp)ME I]4HgТHG^:\<4LGEV(s.)t}v]z.Jp!Z"/O"1-3tm-(DWَև:J^I ,&zxaF!AE.LB+AD{IQ%Z3bnTSojˎCWS 0#a >?-4VLpt߭j3gb/٥A$pM׹9b5EwW=q o y]U l;YH=Q{J]XOa.׺50? XZctc߭!* eKb'atRZ@xa v-7AmX\<핧K.ubOk C#@~DpnXҿ]fc!;0VAi _:Ur)܁`F4E}S[ٮzStMS.UL}/QLXwI]F]:,{05PHFvpi-W NEL_o|ȧCId&;NWE~z|--&K Mp)S(>޴Ӻ2ΧdڌjB5.TA4Ş]Ex~I[)3/LHM}Q56%n49̠,V67Q/AZ 1ŶӜ=*;+0Ŵu GYCS4ϹoCEIe O,d]_.0њ~Q|h~LqlUHyi] 8Oy3>_tARM/`<3OyT׋Iݖ;_.N=/ZzNA=*6^mǶM;>׍v16vן$>p8^n%R#?޻&O8*5`AF5hNZF >(_kvKŏ<9sJߣYfeO*֦AՎ>O.8e.Ɍdm_&am[LფF]!TO7\ 7ox endstream endobj 852 0 obj << /Length 1991 /Filter /FlateDecode >> stream xڭXY۸~_GʂI3㙭TJA$$1<ƙF*B ׀ꧻ7;)K#9w;'N,&SG΋C뽷qimBW ES ]jkowo~ ( d;Yu7ɁHYU92J[:W x$pW8L<"~8*H&d'IfOq>nBw?(/tm9ZIjimc54ן ?$ KCNⁿ򆧋=( px/ۿ k- <b boSJ{5.*Ժ<_eMG}l']LvO4(2|tN cQl N@{"t;t%.!$Ʋdg &,i"j>pzC"chD^J(p2lmlj|OtDHOuyQ_]J8|%-"TZ&Ѡ LLQ+ i+o#En65bt4䙐XUZg'Sk,9M_g]u\}kEZ44Fˠӿ{QMii39XEV)jDVh'?\pE 4٢l^#ʔq0m+m8VUd 8iq Lq5s![ 3<s@ l'o2K5eEiUdzZ;L1kIֆIJa$f,Swh@fJcL!lsMܮ!2[LtH&ED&:8ԁ̓2>$K5Qu3ɪ\QDױZS3^pP @~Zj˥M|AU voyXOWaQeX`[:)c)EnlbhޖE`+ 32Zeta8 _@HШz@ɹt$Ca$:Pu,k_e0A1QnlИhXsIkD0>Cҿة5}oUI9;5Ys}@mY)̍eiU]꟩1 '#TTADc?O{@czC Q OwvϋlG1=0̮*sdMS.~졁4Opv(nj>@r2b;o&Xp"(?9.I z*9Jh@Yn }Q%eRuTN-0`ֳI}K|m:D~gZ+.,SifZja*!76c)ip'H:4YwǶTE|j`3\eǞ[ojUR W]܄MeYt=2JQE[)Bg'oOXŦN0-k%L:{,-cNz 9qSHֵ5ĝAۿ{4FmYdjY\HDSaOdg#e52B[FWՊ^LLÍEn8roa(UGj5{xͅW_x3ݙ endstream endobj 793 0 obj << /Type /ObjStm /N 100 /First 876 /Length 2346 /Filter /FlateDecode >> stream xڽZko[_ 0p  ˊƑ Il}\{Hrt^ }=9C$qŌkv+IKQq.q%DE1Aɂ!2n$GduTmx|_cr }DIg.p%9VDj(EbI]l?U%`@ .&J.f..*DkJV#I4 CHE/|FrķJE㒬:Q̩RyU3z&t4K¢y)R $Y3j?YT(8Mr9P(E %v#npSpr6یqXG{^Upu4%Dz5Q- a" #ژf\XF扱V(T{ ئ0(iMЃ(L1L з]T] a%ʀQ+'\35`5]Ma W1jN1 RP f!P&,&(qG>0`><#vvv'apt4hs7vd2] EӠy5]g2|hѼm^aМG ^ (ތ͎ёk\5owstN|}Q#)/Xѷ\'yzxϞ-HP6(k8px_BڈB0!WxQϺ<#0\<%pG{T˔#v`dM\4. ?µM^-qj? OBk4HHbsL# |D*i,k#'R=#R}IMH"u!A-3Ċ.ic;CleKA<4}V]@&PGd&xΝ?3dHF%X3uZ3Rꆤ2igTA=+qs6Iqmo CZD;Z)CV@q2J d;pPn]IPUbne!ytq#Kq0 v]vӀT6ˈF" H#U̩k~UIkf7GdybmB=Ncv2^ͻtt6T׼{s ayWA'9fq>vO}fjkm˰2fn8C_|]k9jC ?^0ʦ Tښ.&ºu'+=T@_V6ml# nXl*8I$$`I}pC2Eд>CIyҮ9>:jLiΚ_Nw׋_f1\v8zy7_y@FɶmEp.fVa@Ă̧C`Fw2$%۠MIy3ߏ/\I:yW|z)f;ɚ"_Mk9> stream xڽYmo8_Opf")mM6 \vqPlV#KZKj6f8,rES3;+;py"p3|g9w@,|8B9''t"ww rdkgt>o&S^]o&3KǔK\3̓T* A+78|> \ {y*`<Ҏ/hg'vϮ&SݳIIb9U6,(Lhi5*eȽW݄؎&@nN:&BC~a{_o)w$y={K|I3fEo'SݴbaHY  +t7)N6K; Ì/kWɬb{O#J "m~pYPVMIEZnAGTD5oUa-ߠ_Ta+I]"0G<`F%<!,b;;zqa> 4=4bnI= Cg y@\u@jeYv><+B,9^`54'7j?r.F"&gì;Wt-(HRLZ} (IԞ NAX./Ũ-}@Z)VpZ/fi0Rsjv mID#o܆GYZX6uM¡ $K⪕DR#EW&8G6!YO Y1C `*o*ޅ%_Zen2xC9 adCeb=llFY?<ˊ.BkV%4_:Mg ӞB])IpdJ8f l;SںeB)Ղp'@GPA[GмzIYtMe)&%眡XשI<./[|M9ekD7a] ss=Q!8C )}H_+a- ;uP.2@JGdA}?u?~ >:a0gx>_AC.N=r!jfʀ.J(#̤ǴLtuZ6}u͋Ã7I]65?lx W]kR/wt7_]6h}wp,5 Fb)Aa(yN.#^eRٵ)mې'v*0ڄNW-Xh;573rvZ"RVeOܶe<qrۼ5vUwYV'> stream xڭY6$l"K7Eh^ ѫzwrHYMKe{8ΝC?^\^4#vľ ͜*n7Tw+Ϗ ]u od!E' # oxu{VaH:$1MywdCI&^*0Jp>^rA0&1m(w݈J=Į(C^d؈ʧq+;<|yF,HH L GC9+%pz ))1k%V({ջ#Du-" D1FRΑLnŸ۫J0Z )@=bN '7/YH0T $$s@9ݿLqJݗ e 3G>DNr&1=.lFHS/¼BKSK)a,%U]CY,;HDS{+.>@lUߊ,x[gUKhˡk/p B4}%As<2FLD@q<᱘1Z">EC#(W2jpoQ$&N4](] WRq:[kGM+E]í^b8kXme@0PvC2I4(3cIȂTNEmGӐ.twPIK1+Ԧο!P W~JNB!%Xo&SF4( WMT6B,&?|yu˵uR{@R{be(aюJ5%$ )8J'Q*PA)RS:ԢrsB!a8aI0/S8*>YXT-sʬR]Y n  MZA4N$Jg )v?35CBJXb̏ZN?zQ>:UħN'_ԍQ?k׋Ohr;x-7])g6?m m^HGq6?HᓪdPVezyZu йBRAPt  F*mf8TЏļ5*DbnMy}?mMآ@* Ӕ1r$8=pLēT =1wr?6z~{q= -9&OGvPaC|i9h~"i·|k*zv,M" p-ifLw؋(p1L N?^ԖGm:0Eg,% ^p{<5T}^yjfط ՚GӣA9VD\,SRz%QDOhթ[JzS;sl#fg'YKPSpƯ8Gq m#`sc(6ӟMXHhdxKSo%\ܾşrBfTL ďi˝hVJCƌq-Qo)ӹ8$_ endstream endobj 880 0 obj << /Length 530 /Filter /FlateDecode >> stream xڝTK0WXbK39(bo,ဪ=HǦ3c|| C;mS,WR CM)J<"YVT UHl7O,DUv dqz7N{;pZSFCS<PZ C~`h ;Ĩ459: UpK`2*JJpqWTR"<U:T_ ^n.r%̙ -DMY1!p۔F0JS :cx=qq F_D1즔odOi#T Ӆ;pv@.RBoO];dMf#h,h B& ,v3JF9uR /W3ɿY.{w}OڹC򟎮޻&?6{UP>Ry΄$YBl7ƫ 8wD¾G;%WGkGnj_J(A9k?%5B@1~q endstream endobj 884 0 obj << /Length 218 /Filter /FlateDecode >> stream xڍ=O1 Ĺʧd:P]'\Yye~N\&5 c<m4IVNA^w!jZͯꏹݔGc@R۴7I ]Vjmѓ 6[}@hbϥkB=<A?1Wkݿ$Zs#doܢ^tosyTQޏS=C/XI fP endstream endobj 890 0 obj << /Length 2168 /Filter /FlateDecode >> stream xڵYYoF~ׯ  lIL:%y-%JoUW=Y0}TEqgp.<#sBY 9,>Ɲw8c<]W<·ŏSc".{LYG*p^Y>LP8+OX:~)_?w1*-LEd>xJp?/o\ya^^_, KG c 9 =g_*ᔰp2y"sJI*+Ę \߮,w#UI}A`ǝJ!)[B+Tw%0p)< PNZnMLaA%MT".XS'#!d)_a<#s8v[(lYk5-VƮ94˭I23}t'^9JĉfoR|&_ `G Y bެKȞ[KgLMF iVY>Y3$MalazV`70IfYq~Yk[A-FVS [oYgu'Cyl  zWG}YTV2F#W!Zpatmi}Pڐg9$Ӳsܬ@0/rEw',n#G: badxg!icYV85Y&AnjXV:o}R[c/H;㾓'V)c1lhw׺Pi &M#םѤZ[鲚Ca:~|%".;$up1*;4:uT'P'mFD _ f}Iwc NAcΙ)4aFr5Icl9Bmǣr* @tIPhP͇$'t)3t97H@5ʖ @5.0|2Ꙁ.MWC+00]-}咞4WKSgLeY-, ީ OUaQkm9K&0ք)Ĩtw?RTO'M RzlTXƁ{gCJ>E5VFLP ;QPdu:B9V=0Qc6R==Ѽഹ#O W C$J9=5ٱil06uOM#u[nXAٝz4\c g=2O(uѕ(.&9!+7##Y*@cm: M]]͟,m9M'UmB]KBM6@4,l3v3-}w6-˴9&CZi]v){]h9O?+˰Cji` 餿hSB4b\ Ugi$MZ.M^θaR^d Ǭ?hݳM'><l+Cj\XB0/m:ix`L(Za0ՒӚ2r{oc`a`l_". x=k^/cVE[&٨E_}Qo|~#^TUTW31w,"ΌȪq |_ů8aRj~ $w" 5_vGg^bFH)$I1j8Cj5F(Q$uurxaro>6nz3+ɾyFO'> stream xYmo8_!޲doo_vqPl&Ֆ\I/oCޤMrX!9$<3#6ϳfg?)d,e\ 2jI6vIX-[SEmQߐ٘1 X_>LA5Kx,`g*K tuq3+#~cB3cwuB;L5>~"<|\EHO2 ,4E9_(Op-7sêvm6˪^+Nâ!iYts 16ʬ¦P\fs0aњ*Ұ1MGOk~7R\7fs-bӸэY"^HLhiL& ߢc96`șqװbOA,vlg`p[ՆoU8b|Lp+X((Tσ˹ªiokMͫ-_|6aMo]UyN:ruuyNu}Zt`lCn; Jئtss; TAs3v=Ys?GT{ Hlީ"Xb\vͧ k`-M1lHu30R Lyf]"fCwGYN=qSP_SAd =gi*W 'Zk joHy)&S>eXbQ!wi%O%QLhbA8Hr2@ MMh-TpɍμCU @9^ I~,f '=@YylE,MvHq&@QnRp~x~)n>$l*+b$]췃??hrhO -z̯M}'*U[JgeOi`gY?>#Q!x?1`8r.s1 :߭If,56yQ^kV1 Q= 0B6A$ͧldz@-fbh}Y86Lba$MJ >,5RO/$O',^0+<06H㭸 Xky"+i1vC,\ w #:鴓ВR^@LP?鸏cn!k#s@շI];1.`g|By+m#W4 i$4V==S^bх-ꆮ[c.0ɗS߻xT[QT:H@ dp\tG|BTObGRClȝ!Qcq\υS׃мw#؞w~oPQCk`͞~vX{^OXE6%J| \t]l7}b s"Ļ\x"P ]1;}ke|0*T^B?H%HVMxq*ٻA,me'r +\ ]RA5AEƺr|O4ַ0tqblz o,<[ѭ߶mv%R;8--_U$!/P`L[ w0 2w4#WᎬ )os v絎qJ˶،|-O|j~O~Y 'nE#cXTKZDe>cF( ۛsUMqw0+čjq*oM@.L&$b<^4w%v]>D/R uۜbtNC @}!ԡkn[ &OB:^նOS"9LdkYhQIl:JzX_/0&/t:B ac чLLx?\ L Y nfoN:JȺČKL0ee#3zۗ6p.T;IzrG /7%l>i,.JyZ],nvV _v?叚y _eK\ n}9{985/OD6}g" rI:`.C*ƣ;o:`˘#wn,_ dQfiCNfg݄\mEEg݌{Bt,oVz:P}ѡ½f2ÿIإIPju%ۼH ޶9t endstream endobj 914 0 obj << /Length 2652 /Filter /FlateDecode >> stream xZm۸O2sIJK4@[\lqjdɑ쥿3|DYf %>3C. w>_$$ y]aD- &A/n2^s(bO氓e\ /m|B7Ą.yu盋 [DD$ϿEIWv Xt jlY]苷߃L}1kN|x& c{$F9"/jrz+Fr0/4noLxmZye>9az M @陶-^>Մ $̏\ˏs%:3la1 NYE{sIȎh5/[YuNk?y;Ok#7U=">${'3dXbbqtoc>-&Vse$7a2&X}F=>Y]8`{~opCO-Em0HMS2xC4& X!P7T+?OBU_Y^c 7ԨCF5Hg)\}'K.V<BK @ʏ!8N0$~uV"񽛻o6C`jFp9fuu}pTF6(,WV?7jJB!BtT<:`2wTPB|sOY?S$c$82\k3Sgئ8#O{B@ۿl냼|J" V\h=T7.Bv"zn^EfN1ˬ[rبW(^"1̈v$g $:o:k6b(PO4M+3tr|Xa7σԽݪrEKˬ>B D|ct2ZBm*-ēҎaY*n.򰂽FYcޙڼ*hC흡ߩ]g'V#iXlV̡ٟI#~G(/٧*KXK=ؒuY@ ;C?BPzJOe]cV2V7T@]Pu7 X|q5y&k54p>^ݙ[85ȁ>"99,)^WF+d- wdn ~nGύ{$$q{ ukʲZ \DmW0!0[;-+@ 4[ ƎVH!ԠdJĘ 3P0%Djr``Hy>0wgi=mmHj`LdE`6VqFOK GֲB:>5#o\Z<.rK浥 jbôF#|aDN:M;Y>CFfUmҡ}PS!L$|"&"L?y y9$ ,Xy0HHuv+-#_+4Lc-&}K S{K5 tsA|1->)a҅DjC=\XFA_aZMQQ3*9,LDSUd|\vFʣ"m 7i T|2$+*pRHD0Œ7ёt, l!q#ķZKTO{˻}'E\+`uԵOt:@ ZSHKwҚ@\#@Xdr8ꮱK\vZab2X}Z6KLTm}w@m{so`:_R<\o'| _#($Z;D84FFj7Z aj hnԼgEN뱅!m̆Ũe$$ʅOG1(/b;.\Mtoj lGeǧ)&q4W99":]E~"4l3G\ }Q 5; ׵xM;tcΑɹudRH7).MZ 3zep4ܧPYmDDD9~LE3gsW3RuZ6)M cBzOۘ$G;:rM~M3I[g=j?#A&28W ("j{xȜWxb\cEs8ǓgW'>ѵC!|Wַ9V> stream x[[o6~CE*JYW[a.]2c%WPIqXr(D$E?ؙ;ihQ'DO}‘ԑ<@BBH$xe#aIJ "̕sqVXO)HZNjjI ՗1HIb-um s3fˉ?xސj Z4&ˤvo0K=z,V߬ƊZ8jG2ѻMV~sF_b2-iIIas6ސ Ae[. >ePH5,e[&=,Rأ3~H6}q*umY-+0CNqh`@HCF u/ILV#DZPr``A΂%`z s㠽yQ\ +Mho.H#@*^жHJMsp(l&W*4, N[lEo`]I_ !=HP0?<xoG~ݓә$F-JZIcAZEYXlxձZiG.ű$ 8ևNE yCCp QF oHt! -qgVDod\w&) ,~I)"L:6pmp'f( ٠`C'>@;O?a F:0|0t$պ}! qB;>Y+Sl>ϟx* ǚ!|~1,IB7GtFT#ylȗ1$)aS sG ,$u'P Cp‡ x vBF8A 5< C@PzJnUj晴CEq$Fwx/R߉ksuT"ʱ(#bkl]՗8xPd](]̶V^؊y9A0YVP].3=T/],7st3KԮͫjǶZکY4)z; zB:a }dp1u_}@wpU/\L-J B'L%˕TsPԥVwh,2yb):yGn.Ԋ@D1E #s\d 3^%z Rgꜫ[lvzh׹M92#g^%v^ʼ.k:??lJ ͫI\v}oJnts8c}ES*.p0{_vd;DƦQi-cJ̜ Ƕd !A io'c=llYӫZl"H03<[/׵{)}(͢2&@-~峺C^UZl1zQlbEiFAC@}T؀K|`! 9wXD0Tnv,Zy~xdŋ.`vY endstream endobj 928 0 obj << /Length 2126 /Filter /FlateDecode >> stream xڭY[o~`pBj$SऱViP"HKYR$8Z0:;ofgV4 hz{ 4`AƒQW{>`?dݪU u epӽɏ4&#)e8aTY!|*Nr[$Bjn0MM 1[~<~hNjDosPX O{wq& _G1OpztHo#_/q:?hdYG+ǖԓMdgµ.3rbQz+A,%2mҌ Ml:ǽ qr <&H8z-.z$\ՕYejI! IYyuŸCRW.o3IVrp Y& '2U( ru"(Tkۿ'(. ]l]:>`ㇼ"HqS_E&뷭q\ :[&Z؎k+{Ɲf=ѝ"`f%#( =Dɖ\ oSw^62G5=mא?H/h  Ri;.oA xY]RtP@_B  u8[(AJ/HlN]}5<;Dgq>͋ˋwɦZOy6NdVϞ^a/GjַwHHt㚍5/WfR ᦪ)U%<^y c\}|C(!HPnV>펜Sn^jįmJgSjU qSoKV;j۟4ryNuQm=J’tKBIAX#]2?4ޥA~!ֲB34pB׌ʗѿC}-r[{Ix[r? yacK5RmnRyX֭C{X.Rakv^u4n6ܯ>>> 9O;IWF]5Z#+($Ҵ-$[pHE#N]Uҁ 2FH67֣-2,q AE aa@=(484~9Z}zD!Eu s^7M >Kd_p(ZA&ExʛM̻+>8"GlWUq1gPOdX>M9ROG{xQWv莖Bms]\7a bf:vQk7cơnoTѫMh>0XQMM'DrJƕ,f3n~47c>ś7N"V+;pȯۭ[E"a V!Adzhӊ +qQ?W5^Y7yu}çv 8ԣ 4%#X8 endstream endobj 933 0 obj << /Length 231 /Filter /FlateDecode >> stream xڍPn0 %f%YϵV &b;*U<Ȑ x wa^C RGo VU/!4YMSa> MiwwX1rKȑ -Pd$| Koe\{x'_dV(TW*BlT$)Smu`NidQ~9i{q{z&5p&Фs/V endstream endobj 937 0 obj << /Length 2485 /Filter /FlateDecode >> stream xڽZms۸_*͜`/I;盜r2|`$u$4."E"N>gabޛl?F!Eyw=j4P j`M>By0čWwC11xuv8"xf8~`C֌_no<ܽx0ywk\߼*wNƈ0_&ݛoDiH2KHSr?ƃ8y&*ӋϮA)A) `=0bNœsU<41teL…[h!550j-έG5O:0!(dz~)Ƽ,!ڢZT a80Pc-wvvWD?y`˿DYS3<(ӟ^6pf˚P@hc*r;瘴[!G3 Y=At>G/ofuF^h|(]"%k-BH'8fQIGSEj*5V)}"_A2^U§`:fK|ev(% \K~w9ʈ7~//?܏~5קg<;gY9hZLm"~d>[VQ^e.U;]v%o݆U4ԄR&R#F( eL:ID_bxJ?q z5lw@Aذ@ΠӮyWvQS[s[dA+GS 9hA8UpL#, [W?hKᄪ.}h,l ,.!'/疚`yF$8qMlCF=iR =:gyOK˨,_,atUtZţZ7hiVZV"\Jk_LaZל02H"M\t~CB,4;$ꪍ]`p,yԁ6A[~Po{ @a똕ARS wdQ׀ӽenN A9? f4oۆ؍EZu2@ %ȗmQm0WQ1A{pDv\D"N{RRt فTWdWTx́s`  @>i/!z{zU;@lꆭ e~mmk1{6XW;D@Kg>[+ΐ?H,/;tݠJ`TZ6u_C?QA_E H t_㼣> stream xZmo6_!i$\"-yE J[IkҮIC,E3Ùg3c`7'goR* A%$JEpËE,vWRP# \!:VJ ESN~\4̚Vt% WTИ`a6<\- utoNL%%cIJ,`ֽ۵:Ff̄M ʮp{ m^􄫺sK @F`[M-d A1e[^TJWAsVXv_wXo=dHvK9XNXCi ""%'ш?igYLOjhI/"o(JNyΚ܍:.́ NW+l]ڬ[;ne]TXZx!aiRЪ ݮ^0zľ.jZlF Z?vM]X ij pAS"X2Ex.9ej]۸b#yg$D~(e6whySTN:!pB[3.b>d`tLu4-w ',Cc&x㽘#n&vu7]߆>t{t[l`~b>0)>B$pY 7x@ʲ=TA~`?`6[q8v -+CQL0պV{aA#dwUrd19 `#9%pcD4-.Kkˬގ*6"H)T (=w"EH0 Ѻ(|H:KLf}]4PE|J'_?~M4WMD#f D&3$ECZmգIb /n=`ᮢp9NVpia3NQ:-N^yA9<%Dk|ʉ_ Yv4us$ʹ#>JlcA(?:dO<Ǘ}g_)=HBb(&R"=ri1"?ƃ> .E_s]lm#i`xf?K[{;޸[|ׅgʾ:IE''=Fӭ{s0VTOZYqc0^&?׾]% ;jƃA(vBEy]֞j}5q/ )My"m kd ݒUxEŻкòlnh՟JLInbD43/)A/Ęka|.>'9=u{fQ}9wN> Yg5{gߌ*"\gROE |8iY'swVDLϗk%K4X~^,??Q=k0ex_>g zR``=o72"\}<Eיs8eiu5Mυw$THT6_UaC|e/ ?=0mծ>Xr//ctr endstream endobj 947 0 obj << /Length 2796 /Filter /FlateDecode >> stream xko/hO>K륉[H--");Y>ı|E0`.3Y&g/.^*,d\^*qT]xMXʛRIjuڲY,M5yU>![ج"X˟^\v&DK@Y $V۳w` Twn6Q"x{3>U^| K oTxP&#@U힆&+F%1Kh9dBTPvhhijSTSl~if0*RMvMBkz)vWX] +@CnWDB0o6ET\- 7UUKT`10f󰶄ދ!8,Yz᧝ fJ[ĄWsq;YfX t u]<a=Ǐ^/4Q7Wo#*&F$Emwdq:p8ci-;ϟJ* Olr"*,s;{`zບ96 &r^KY-gk 3]VW/Auή\7nT]9?>'#qߑ܆B:AҖ+ Dv>D$xVA3fG3 އG稯h @Ǚ. E"Ç#(IIg\@)$)V. U~'SfGitˉaINi҅lc)X2IZ]͊=Q?X>Z^iT?B\ |wD"gY_Yqt" w' S􇌛[mNgt8jw8c:7]}qq+ YOnӶ.܂^e]B,Q'ܮ7V閎6PL @pRU}4NbG8qz-!vp\w' 8V8y`$*0#jvoY{CÁ;MCp|meo"QNkhMHoEqFuTlx8өW3B={}mlI#o)l <;u)`Ի%ICw!ҹ#1} 8g﹊#$_cUc Ifs4a2NGDap<8l?qkz~.}묩Z-&D}In6~+mˮE&ӊgm]P4U⎊&mE14,d&d ;h"p MXh3o|,[xBpeΛJsl9Gtw9m_/N8@ї<ǃ/_&'*[4H-+!cɦ3Ft5dm6mwkI0-XuΛ_r$rtͺA!aHx2/hт<7$c'&0nVw[a{e߿'r>|*'j|IN 1 sE2kz:ǓH~^MOʸP܅.s (`qk 3 ̎E۟_G<W$oxcj6Y8D貯s//Y\nFB&ARYs6Խ6OlVBDF ?-h 2L_58v%_VkϙksbL$a/^6hpٲ. ͘ws@=KrAx4W'd|nε`\ Z'OD8r?^vɒnawZj\lٜIXg?Xs ==wdfZ .f>Z1y!]wڛ}ފ4s7\P+mg5LdJ,w:>$2Rp?%NxcqS=,*#?aL#t89gʼwXnjXqu \0]ZA]+ U<, ]>z|>چ×ƝdK:D2~y 5Fbl.HFUPE>SNq MbD*Nw8Ƣ%%\I쭋# MM2ziȕkߣO:&#=:qOY:r߉h+˽"رՅGV^0C^OO 9}Aծj#KUz$8SQ(.z BڻγEq_|a֝I;nENtp %S6kz2b.k<rQ33'񽿷B4dտ;kd0!e{KEz޷n"Hhx&Ŕdj endstream endobj 869 0 obj << /Type /ObjStm /N 100 /First 885 /Length 2125 /Filter /FlateDecode >> stream xYmo_ٝ}zIpm@҃Q>Ɩ_DC˿3\[mRQKr<3*)gJdC%MdX))=ޑ)x!pb(to!yRJ$bNr&'\#&D)xī axor0,'#DE90r6>z  YD [ ~T<ELQ_%)&E%Ĥ &u&F$&;)b2A >1H T8a`q2QD hdjsq.X`kY]B7<fQVl ]tey#V<phR}6/ӳzRz. o|?vqZ/;/<|1$(4?`IJ{;7Sx9VW8~}VWY?w+u?ޭ3~hEW~~ b5`P#- &p%@I)ۣni;kՇ_ߜwUuhF"18M8DUm,eoDwww^\MgYUU3l(8 kM1v~veXimu{%\ {9h18DW m@-|v1/e=]͢//Ӄ}`elu`[Qq]٬==uuݜV'`,bf6/%9Ke[_\MM4W$y4 ! [M0sN=oC>m4[:!%4 _:hDV[6zƟ̧ұ##Eg p~{uu2!t X{;mr[rё!$/)`Z w+[ [ gr` &~B>9OLnmKn/h|CJ@VWn'3YB:PJV^+:H권e`y͸r>A ցါxAQqw+]8cj{;ެ<^,ǒJHu/.A,IUI5[.oVL2omn?֟ˀVƢ])iߚg o ʓ]׾CUGx'$a0ChE jzIO \]^9Nz JJ\@M `5.M46Ic|Ep^3MťؠqAXf_q3kwFm/_D]zQcj.eDi f0@ JCb1u[&i+ %v^9 Կc|"oG(%cC$y1<McP)m= 8k,XPB4k8?l䷍̛r햌F<q\QN2a"mKWq~_)ƚ:ǭ1o"TA2b4!g`;J򾘞j颾Yo*'Y=ڳ _N_>9AP[3Nhj6]4N0/n'5GE7(w("-|M/7FG^Qౖ@m%uGN_LNx`=x7XOnF;hamJ-0> 3 gLiKN y숛&v?Az]`W!]F ~/g]6=6_ endstream endobj 953 0 obj << /Length 2788 /Filter /FlateDecode >> stream xko/{/Ҧ4"I. RBRܿ,ErbN@s:x3ZLv+gbϮof%663"]\$^[WuhwE]SWuP"rg>L*9SڊD&l _̤0Y:{gmg6N,gWg8L Bgj7mHI!MBZ V[<+ VمTzZUŅ1f~UT++/_EvpHm"+T"!UlD4%h)AN* $Pu@C{s:*G%/Dj Td1pmMޗ Y݆+ \Cw;!r? /%sSl2=+o}5wylc7TmIwG# ф~r%psg](䡮.VSYHǷӅ&"2z0it* |O^-4+$.>nMD%oY} 7tzrgLHf"PcMȒto4}H.Bjʁ^zVIh181a(ڌ0#\cȊ5yf$EVyur aGiSߩmh=-WixQsD~ۏ,Di"D03/\(lrt:!^m;$h-}hh)pzZHWͫ5w `쁎tyK CLH^%F 5L0u.0dr۲FQwX$@uG߃ۇϤTv pQ~Q[ZS4c!I@Fvpkʜ @i4嶠]R#8?3I(bRosxLa1ħ;{ ˽;&ZT PR&IeH"I: a]29x AYjM=xi lj{NOjt٠x5M83u,Xr_WNfqbH9Ȯ*Z1֐/.ONfxM5rM.oy *e?c٬^l[jT2zFt ށ%YQ=5Eu[2]S\ڒQ66SQfv7xC$l}u𒈝~X)s P/e,ʢc 4eA[֕o|A,$[lv#o}|NO}>V(}U{dxj"u^AѲ:dSe\ ?Q(B 3?)oUUl WJڸ$gN8 fKMJ; hynOy /}d78cw$!L:x Mi['Ps겫-HWΰE,~#ߍ<yEn1Loc m=*^mcݻJ10+Rx'nK'ɄIOY.x`!OC`>K9ww^&4ړsMρy"%"RY&$CrM _9|~3S9)Ӟ"oW "Ma .=vHIѫY&LHۢnCF>? r!oe/fD>b࿂qm-Oe?x ja!5"N_6ɦ V3M TD9QQpYPj-g  q:&-0Ft1F63՚3sg؄x@8180q0 endstream endobj 960 0 obj << /Length 2595 /Filter /FlateDecode >> stream x[sɗ3^xytn|8'5xXQ$eTH5owA]{ٓ˳gJz1xW By:b:V{=jLŢL^Og$"D$"Ƨ^p홀'PiBb{KhLőwc{e"θ[kweʙ  W‹WfJsءigSvO⽞O^yZOe |ڦyL[LF,oܥ^I2_j f8CQdE:T%*bʊZ*%tJ}q a> >%5#:{9V\ѵK֔~Φ5 YImi|* V#4W4Gz@$5΃b"a~ rO-߅f^C3vӁE^2nZGoi`ЅjQh*7 8%pCv-+"b]]QM7ҦJZOENW0t)->col6αL9X78S=8H ^CsNۦL+-ʻo\٦}C ŵ؁\ى?!&1rF=1ԡM ʊ#*F3G-yz}UL|͂X,>%8J-_iV>h!P+qX>~d+U!j,QVDsG7>'q):EZ֠L9w?S)n*VU:ıF5%G) 3 N6Vqcn4\熮ba*:tPY4+yeZ & Zr6han2oM S%uDo0j.3@1#=AipMVnp+6iL..َM}Y =̉\R$Z{1*=c hjz$U` tbwا :jTjxe$swm˵)% %w˗ 1vX!]\&쏫dTm(63}(6G@6f\UdZʣ8uT܃WUmgU?3JSKӭvw\ }tWi]f(N fPW} =ٓ6r@Kc̋ɿ4_N̦ -l_FIXVҕ )Yfi[iymemzYB+r%ӧ,!a!XH߶|Ļ(R<8Z`qSEQ=uȏ2O/k8lp~(XQ[0wPq eR' }2>p,ZO~*OdqYkoe5Ƹ!rWM~P1mű~ U|E_Sˏ,)n'B,XrMUR|g%k:YzpWU tT$LL^.S^7r`,nCHFe 4%71JU".109x) LK ;yei)bX7,wxcm쨹+H}A 9Lt/o۞}c' v- z8O\jC)ߥ6 ?8di%i/NW6zXW9&yG2DצyQ#9';:`J(0U[܇ %]@eWɇ1)> stream xZo6~_! Tpwh }PlՖ|l Iɖ"/֒ه صDQp7C`\]C %: b2!2*5Y*bxe5~0eֵ70懫\P4`N$1iJD6:MWԫIjwOx$) H$A(-$ ƍ wmқdm_5Ko|PF-cDE>P=:;sc?{Xi'Щ,Z*g JȢܬ룑l3)[+j̒I_>ͅ]INCxi>pGyVDBkC @/_ /a{3Ha?̴b{s700t6iMV E殒`D4c=٦ҜZαJ'c'Hv6D{8`  5HXdۉJޛlf!mw3y`K3m໦̢V/׌Rwi<$\OpS11pk\}OeGiV7S|UsXxg1sT150v^aO<Ġ"E<5#Ԇ'з6.ƈ08炀òl??β;)ܣZyzEXtm6n: SK.sC\/QksY S>z: Nn-xhI(SÀܷYփM-`p8XHB;{#;deRfodcsV!тS88<|I{԰IgMi#$qEO`4/y[Y>Lhm{Hln (8:L1y*0b~G3y3ɧ2pGQQ4_Ƹ9DHecA3|ᄍ^D"DÖ[]5K0{ȟ-"a$t Z6ߕN Uvcvњe­O<ݭi3AkXKd]m=i)w3*wcP \*KgvBSt)A4i?tl0r,\ ږ/@u^(E)D0Az-U2$I"]cy_ |yS\ %N&,jIT|fBj[pL^~s,9oOUMծ*ڙGw/A&vn,`~Trk]c} Na`=;&5;n,iWmc M}iʒT2S R.X 4R|%1k{!(]"'V'oGǥƓ<)/ ÝpFu1P}A0g5l:]ʒc{4X,b5)c3]IL&)`5 EG5 {EWT9s+rOm/+:&U&ab¸rM.S':dK> stream xko6{~/HQ}(p(m(jK %Ynl6B3'ͽ;{ߝ}uuv^’HDխ'#B=,Lw~fZu[.yΊfvd,3X췫Ͼ:,xZiXb>7-G3޽(rt3U$=\Vy(?o ),juִQ_7C2eXc)u^Mfai؃s,Zl&OmUTW+*KlAL(L*?#,]}bQە<ED):DJD(C!⏍Ace?~L털Y80;]9ORG}HavdEV=1 RJxGgA ٦m7DZ8$LT jɊANzU묮Z;igW~y-Wz@-oŽ+< PZClۙtYe`hl7P {PnYmxX; =4Fp#*;*oBɖ,(%qC-Yc/ Jʼ[B\) ΂`8lyceYeYҐjDJvB@!$|.%8@_>&,i 2ʨ/ysآ$s #dJdx0~DtWd (9t($>!B8PpIA( ʥAD wpD˾Ic8w1=yN Bc`Q H{13PUg 4tQsR%םMyөlSWNT)ZRtARToP;>8,N쯞67 _۷]V]fskT,b1֨ZݟFUg84IFd:Nke.BT?25eݺ)I#qh@ItXXCrsr X8aa|shxs(ֻOjg #m.wmƺ8AlsPjEt"R벲3L=ppQ5JyE8 9Y1w?vG`=ef&b銆6Xu..>(h6J* KHN2Z(/[% X"b"ԤwW fxI4ÌX?_L@``, :]Z\dMfgL59$\v S-ؒRaq6~$/?6i 0"'!%JA2/qsMv*%BO%eUwMdؗ]vy/ G5NEL0baams]i*oқ|7]#pB#ƢKI6Rҳ GA/r :b!:&hxR$E$`馭f|< 0o8ʼuK☌)hbؖ7P&sS*1v^d3-Vv8lk'= ^B7&m"ʶe3夹WqyG0u[.Lޘ%Mm)g6A4|K4VɱwB_bӖsZvE˶z9wX>=,pi\Îkwݘ Lx:uR H$~W D%9+xi^xIS]2T1~WA;EكOz* S $^WVgݏa<$Q"o>+awk4ޥ(=[ups\ƩN.otI]rCYd̤,|3 z" @ů3dO bsŸHлj' endstream endobj 975 0 obj << /Length 2480 /Filter /FlateDecode >> stream x[o8B(|W3|:݇}p9܇bJ-4H6Ѵ%p8CD^pqvB(%:%b/}v䉉V]^.Uͳ&w F /~93]ш9Ғ$4V7h DD7.s>z6'a%%\ lYTCA櫻1FRxčLIr~7ћ4~ʶ['q^+[8_}2A/xڛ-~WVv!i\]f4=Sq՘}Yow f40.Us0 h'q+ZՇ)C{k*1̄,mǑҸ:E_N]=+`r+7E򯮣ڊ?d_.1Tja1x %?X?q/9ESB^ Å2$M &ytցw*O98jTn9aMi94ZKXanol=\Q@f%¥[➃wzhH'HT]XN9NEt$`{6RݶVvftvhu+}?{|T(.g,eM1^C4ͱ!4ь抲kya|U' 2vDY5,#3p޷3=c3SNg;o6m{gm>[sp?pi]z' P9:+!*O:xc)_]UU ".̪ŵ&L[.:Gl?kBOөRdBal_evYlvMq{{=smQ,=*H aXS}޴IY8GFmv_ \g+.o7z^/8>nhj8'ܫ}d)>t Œ Ydf?Bep?8L1*4j- gĢ% xѯ0- ̓5d(nw{KC {t ;66F4Xch"|H$C#АР}8ပ2﹥ۍo/A^jK,sWca^7\"̤oY]3CQN)1m\Ѭ\:ӻ)[VVx.e|M@U~_TpK;^a,R9vRG@3N8<dysZ?"֓ؕ(ORxU̦ EYv]TWrA\ %o|ֲEɥh4OЊ*GAJ{PjR(;l<'۬R"]/HNKD(\y40 6U)̌-GUV&$6VƃNCl02EB~D>Gx@~{/tVȭʕ'Biʕ9 t\}u5!;>h7#MGkb0UVƷ˦ *װQ^H'Ox\ji3  #{oB&w*N- Ąh>B׭>֓}@hl5@6'AEO~|>^"K^۷uu{*t=/K$n\chxUpGyFfyy#P^q(O6;E9,x@}3FZt*nd ~{aO\7aP64\Y,M C*jOHM}YK*q=MOpG  O i F:(ovE:Q gZzTlP.ǽxZGn +2} gnmѶ[)\[plp㛑੆69\:_rw+x=!)Mfɞ#I::U=3BMwG)h7D2XA`'~:y1q@m7BG&-ЖkͩT\^xKhOȷq0ntQ endstream endobj 979 0 obj << /Length 2331 /Filter /FlateDecode >> stream x[۸>_*1()M٣"qم!X,yut,ٞ$c)EJz||Ν.&guB *aGCLGw2=һј{ͫLs2WF&2*i 0|}7,g#΃~jpoܞ [^_DU`-Anul#1.O AB4|{re:6 )z7z;ca$_e5/3&se/ڰ|3-tV-2?^$`F!G Lsnls!L xlu6H6?eCnjM[N[:1ŝ- 3D `C' L>G5E=A94p)=@&jSM%Redۄ 0jdٚ?GCL_6/,f:/B GsH㲛TQ[Lqh*8#nkBRc*8LI>`0pH= %֧>4,"^pfYpĹ@.<%F㥝؀lYbZ#B R(਺xMH^O:"'3ENxl[Fd~ZENU[7f.:+NWm@>R-#(6:!ڈ^&=<z~!hrce51a%Ånq_ X3sUitvH?Q0$Nmvvi`*}C;[j?k2b:J |5OKzL P},&.띸v pIyH%p^WmHmy.n(LlBkP\ۄPE L"{noFT\g%0ѺPkeţkAQK!NC-륹@bF2z6CލޫRf 7ImnXgI/$pZۥs7j7WU0w3SC fŎ*hmr4,PB~g '<nҁBU95Tti@'Nz_j)uSj a{lsR gʜh)4+tMꅠ{pWaqӸ|<*lTՉG/Չ ?~z)P&&DáQAGzUŅBX DM!H cO–p G; YTՀ{18:b cD9G8lN,V>E_?= {|μ*A`Dd8$zE#Q5@#`B"``H d;D>7n^]X,lڃh!IA$ Gco""l endstream endobj 984 0 obj << /Length 1961 /Filter /FlateDecode >> stream xZsF~_]7ЦI<>tڌ%d3 }nHNcn2cN˱}Իًӟ"i ȐHxӅ?/h̃Y̷YW*-'($$.HH٫g CՒ4嘆ލyRp]yo~=Ljmyu>W_?^8}|ĩÌ`"AB<8j ct=6}_4 Z :MOQ#E$45fh6;@1MhEVrXLLe,Qx] $R,Yb/^=rٹ!_fLd Oi20h)̄),/f_nL聹IGs):/L4nD$ j@ns74\8JIW578v]55*ؐSc<אfI dP:Y:nE$v`3ߧ\s9#?]XXaw.Gҙ}ZǛ%_]p[HR8" \K[oza/nE-V4- ]?\,< Y45ll 8Y+2[a4_yZ(Vz2<m@0{8* 9~*œd`B+ua#|;02I$ Bw\F$!iPذSϬS\#snL?F s8gT/ǦM-;qsWt{͹$?%m|Q$0A$'~C[Pױ= 5'.YZFo& }OWp"l?wXaAb endstream endobj 988 0 obj << /Length 2205 /Filter /FlateDecode >> stream xZYo~ %` y& %] hm+q~}yHd, G/VWWWu<x뛋7J7wJR4H#͢L7cxHR:K+Swe]ԗޔ&o Uӌ/~7_"H%X S` ?LvTDg|wO&@UdD[k|?Nű3RZYbZ-&9,nͶλEī72XK1ip1 :-ؗSL02D2gFW2rT'R[3bXȢ%}kXJ®Y+jTZ5uW ivG϶fv@MIBn ݦʴ <-k'|mПewiͱR؝,Jn0cQT5W]ʳZa"]TF5)7f֬@OlG=DeZ~n>ȸq~mZaΟ^hy"P`%Nb{*:p.5}Q{Gd8*\tNf-,&(&*f<_w"Y("Y{<^sOyeHx|w3nqǻXWi3y;pGhK/]ǜ;fZȩ}-sGqOdPA%}R4h_!u| Iu _m~[x풚lR-j|1}Y`p+{:x֢ᬱVT RLZH*Cu8;`B|EbF.XxlљG2(FCm^u*/ ZWeai1[ 80֮!,~Ujs/T F 5u:x/Z~\-5πz}Vd<&.Bi7'/?*zo}S-06xJ@4*QeRA8 'fR $J{T>ڭ|B\ g:# Pi\ 5Њ*#__`üz(_,MDYw%iġMdDvkwUȬٙV%K_׊tur1 BBIqGi:{kBt ~O&\9Q#3^[ }+F}g?&CF7?Dh|k<QBQ1ZXVӐ½?2zʐt/l߾ki[G,s gDN̎y͜E)Ɛ)0e-4A";d)=8c%XȾ<ʣYlw 2:#QٖBK;ŮUݷs~QS:pI'7nr4IP胊jJ( sN-h$(hRHfΝQ D,}qf)8}*yGYݵٳ&qNI!|uN2q٣9)2PiIP4/ILX~R, uCȐ"( ;ZMEu>#b)LZ6Z؉VBgh iCwBN.߽B``K!u?L$2΍c.xQȼI,G4bG;b .. ]<`ݦ5FЖr>5w&s endstream endobj 993 0 obj << /Length 2503 /Filter /FlateDecode >> stream xkoFO]ɵ]{i/b! J-6ofwI8'@`$13;3;]Qܡ/G;9= IrN;/y39rRn`sخ㬚rUI=2/4|0:}w'G1@Ef@{ħXyG%?s(a\QkSL؍Sn7qq/f>4AC' 㻎XnȄ3- ‚` C1(X֐ʇ˗MX`чAw#NPoXh"wOu9%ajt DG T*VEoYr*"'*? z2uUk`2|o ,lI.v,*߄po֖Jf8] :3=3 w{m{wH;a 02ӉV z{54p!A;zm> M lڪ Z-{h%f*ksXWq0kVO]\r솯vq? ߞ?=s|tb)U ~OMHR-w+n&22YBL~ ۓ awrޤvk܁>Î澓KýL~%-^Ȉ' J h c!;)#hv5*0_6E@^~=PbvG^07yKY9FK($s}[viT2HЪM Фq Cѵ}u^7V| C&Yv2]ηEN%rn =U K*l"V "\'7(0Hn0cF}ܟ6:H&swSċ w&aDK;UxDwJVK[ׅː]U#J# IUmI'g\MW. }CC5l,3FbG36K܅N4x8Ʈ> 5{ޕ}/-T[ JjƠBZ-A^4peN@5d1x@]pǸ$ߖj<\Caw?<qZ ۍ}Zrb4$,6_`M 1g^ endstream endobj 998 0 obj << /Length 2518 /Filter /FlateDecode >> stream xZmoF_ABzIr4_ ūD*"7/I˩C*uz0`.ٙgFW~8y~~rB(#::NIȔLDM|>KT\ʫ)O|6e3;UqU2+0:{{'OG#%DKɛ4Z@%"Kk::*z} Rl{ƫ'v<%*ȈN(ahU*:ʦ(wfJ*_nkmfUvɯ0fq[uF(K,2y񌤠DۢZK`L?FW&Zvl6u}Nnvź#!K3iY Mu9xZMi [% !sxQ&LQy=B3'AGo w!J\9]LA`ny9^d|U|@?lX c]q~TZ8,̼X׾^w*KiqJ:ijnk⧨wk@GSXzLSL80ksvw+RkJkE9癦![|:ÎK Y!8p߹0k׸(6&ҩl{Fa$~Ź8U.RUQ۠@kw]6'~=m)[%D 3f.$GrL'\ų 9~k}r" )2< IjL!Ҩ4H_/}Y&|.y:8:H ix4:W"8{4|W^늘 ReģA7!lqSkLW܄h] W1LTɾTeM#u9׈Tg/48{'>9g<ٝ^ +d*(2AD $-OkG?]I~R<;2H+Hyl_LЙ endstream endobj 1003 0 obj << /Length 2244 /Filter /FlateDecode >> stream x[msH_ yc\MmUn@Z"_=`w/*343===O j5㓳 +k<TbZztyjE@Dz--|ɿOh[L(C1d:b92ȑ#&%>$5ބiK_?"^.s$MbN,eiԳ$ZSz`gra|I|:Dn@XqK7a֕ZfB=t s#f&B 9lB3qa,\(MYN)>BwNϞ#pw$R{2mXB, A x=xޢ̤f:&c::UC{ppԘ ˕^TR=c,I66pm@QSÈB؝#N[@E;XmfydƗ~"?$JfH.!R 5_2)>K j&k8Q3&svZIzGIQCeLЛ݋V}uPdeb^^0|izFa>fˆ4-mIq[6);^.L!UhVe\Vr=q  6R:X2HF5YϷ ױ5׻ϩv{WBAdD]ʕqJv8CgNd̹Hw(|G׮G\!k%Vș*pzbQī N&L( Ocw\J;pxH14nk .WS.&WN}U8⬺ :.pd>'JQ V=joZUQ \R 5&mH1>@? =߾fwW-&F?IX@&Xu7  )Ĵ^^CN1҄ઃ 6i⟩Yba4;B"QG lxw}ZO }pO~ABNũ1gE\,{RpM/j ƚSн΢,WN|[iUvV7 'IIsV=^7z.W1v@uDR \~!:i / 4yf۠ޠX" obF`,28 b͢4.`@M]ym#Y8KyNɬd!o! Ǝb,w]%e%$n n, b lQTݫà i?(qňc0w9Twl8Dgcw Mg\r4p%'Y8W<胂dDzHEL{X`DhpX|P4#MF‰z쥐8u$մF$'+úBtѧu[Zw+Z-vD85r?D;8ͳ2ʢ49$_gq8χ݈?h΁z]׷LUWrW]er_ &UxjP^REFU bWEu@jU pR4ԟ[P+<6>rl4ERUժc)yVO.wiXg wqChV2ccSH]cVbZUWiX&%Xx+J2Eib #o ʯVrxMk7O[4eMMz,\I,~"9|*-Hױz` '!YɐF"Y.SSiY7_MM#Q? endstream endobj 1007 0 obj << /Length 2220 /Filter /FlateDecode >> stream x[m6B brMMršh֦Bdɑuo"[ڛ]iu+"Ù!g?NoBJ*cGCMfb.#zr8?W e_ 9G<i|bIfI5j̋WA,Ž)B'$zҡHP1X*uݯ'fi 64Le>ZFKbd'mh H2b`-kl6Ѻ ӨT}-'pg9:|j\EE8vR4B#!CJ.. h_8'Swx4Iz?E+5 Q%w% hy8 I \UP0qI8".(l &C(@`&:0q,ܓ!h0ǀ/ex+ bvolπ[ > 3+`N̖rX޶ke}殇}hR A0&O0EsHl44yD=4RmN#1AO/h^ | b'\˭&ۣ3P@gLs}g"L͆ng6\^Ur ySe]c÷R>(9@!n3~6qCY)wSW-t%qk">Ͳ2$8l5dM [.J[Ry[w n]r]y[Ú΅Uĩ}WkyZ4\ 0fƠxyhۡavyk[``ΛJzGo_wwڠ\< 䃋ieXzn- (XnQ}TnҚgcDJQ_I8%B#SԵCIPU]`r xC>4@ 8uxS-v oѮt xjEͱnjNrA\%J8z m S6cPsA,||پ1. l_ZUHKF kZمlkAuaVVTZX5SU4.gR-F#&>yQbut^uRWVf: Rf9WL hiIGG}$n98\T2 P][SwyٌLレ?>=2j)ϬPAVNH}?oQ佭"y]v*\Ma[~ޗj{n\Łmy'-]VjmmΪەz2>SUQe77ɹa7UQ5 Z\]ٺ-uYZ't6MsH$<[~vC.xTU-h[qCY2TfLE,nqQ3oq!b 2r3v<x=€0s6Jغ%̝ZK@rwãv8n@.nxg7wunO9 :_ZQ@C=vЋ; `!PG{(h>$6!%NYn?4cIq¿`; [U>0a}cI.^X1A;*]^|0LL|$a@ï~ ũb.0ڷUdOn9bm3jJpDuu#DÑ*44 4<WM endstream endobj 1011 0 obj << /Length 1828 /Filter /FlateDecode >> stream x[mo6_f5wR][ ui h;Cċ-5~GQvlimn6bKx<8:pt FŒ4R4R\#k7h٢J7~v͖h$Eo>tGoܸ4ǯv|!0#͑* v}Q_EXGO ".5|;M\EpK9G ;b$HRf E/.0)p4(3M*8Z3b)-s1„DLpI)54S$2iME0 \,ͷUB|F".-LgE5CߧwupAݫ}doj]9|H(εE"5\(\aG:M$nAI4P5Qq$dj>KK *m9xagb(5d/̕IArWN΋M䗯޾<ޘ)Ybyٷ؇(&.2y l?䌼&-nzfK*Zfi}n}A][.[rܮȸBX2Ia/1"ܨeLY{fJY*!+T v]b^/{z(GT{|SIZ582٦rwxӫu%fr5Nna}zN6`&Ca`?ۅ+rTsݿ4,p– ^BkϺ罽PMTZl ;1q bk/\H'wsf^'),a/gȂ-KR1qR5)[9Q󻓬]U]1 EPr\6d+q{FTHؑ%Z(뫠e4 7oi!+ฦ!8CD0"UX!V(JI:47 =%q7]8ЪΆQ(NW@a?0v`xlo, eA4q/ i2$bkVVGw51i69t Gw1hU3,aцnT5+B͂F|jp@v4Y5 pbphMZO^Wx>[\fy2PmCx| UV.aV*Ɯ<;ZhS+ ej:!o-p94TZ)w&8Eo~I($tc}ǞҞ8=,uMYI؀4&^ͤ^%F6PK&_Nt*e9-Yfno>|7mJ! 9DWzCY(3I;Qn o7F4fH^=DpE 2$fw~/{>||9IvDl*Ja {asR1ؒU^sL]bX_=X?(8rÍK^r7[$R7'ho2wRfK=vmh\fMk&4[M͞Nmv endstream endobj 1015 0 obj << /Length 2278 /Filter /FlateDecode >> stream xkozZKMh6 AP{~HwJ.!Js^`pGG\iuqWF#2ZEL*ıO,ZB>Œ$ʎ;S6&K6Ik (Axۋ.~ pH(HǟqFL'Gjqn.v6\8. g$HR UeG"Ώ"W:@ҍF BzK0!O$b[ X8W (O"J " $F$‘ԇ\H~Zb "$",Bys0M~5aRL#5v*:${ 34G+8iF _e۴y5a%O;?'KC`)y|~UYyl2M毦b檷MZtB=s!%!H ϾH`%;[I%wsyH%-ڹ W!-4꜀GK l6/7uk`eCZ':ó MP0kihgd&_QѺ/J[B>ILw]ݽkC.T̐0<2Ͻ7PMdF{N~(!Xz{ֱKNȉI1+z~)9/%R8N,,-f xɕWDW^Ѹ^l FPIkAc'3Dd DBWJk8H)utq>I#ZSp!Wx=ֻT Fn߃d{;{\ͦrdoa+8m{0Cq~Qf[yzlSn ~im&P4uY;ںymћuS4 i5P|{l~@п9o{ 70 RxN ,@0:pnOp4#BҴ[K.hںpk {Ǽ(HYqZpv@eB셤Wiso__5~Em5d4, : d>g(7QmJs-K?7[qӃvA- ]cl(ru@'̘5Y2iGr0ʣj\@i=b,ͻ"m,-=L=Ug r]>?0 |t *#k%O/wA&]XxX0Wڃ"l?D{wpWЛ181PnBGš Ga!\Бk?t [k5\\` U2`}K\`00X&߂=̔qmOXJV"<9˅o}Az01؁83L3F8-G+tr?<8"HwKƂ(C 0 PI?uH{6cH<)I9`DmꡝhE?wi 0MfAHAU=Jz!%D>kC:`xcjd}kʱd)zgp CmZz>[!yNI}^. *K|fYU{PAm3 9&?{ALS><2硎#=G#Rf"txb4# ["(=$=,Cd b/_̘ z{{q,koW{i!?}xpJzoV<%{y a] 4D>cz0Ya(Vp))Hzʟ~U~'Cb+,Je>=Aox@=4MQm^XO|j~K`I3P™2TBgߖ .ȔG5-V\:pf=ޅ߻Dw%b6unEOJs>A O`xz Qn; ܆\ȕ=;,wKh7}}|q`贋y }H;K. +h/@Z} ^O ^OMz=Rp./& w(|RYɼ7h 2i/f$<3GH$QJ4jR7|{*8fo_$PHЩwm.yDO\A endstream endobj 1020 0 obj << /Length 2227 /Filter /FlateDecode >> stream xZܶ~/^)zI/irޢ( 宸^5Zq/wȡ^{{HkX`E <~3J[z?\||^J(֋/ i2Z¯bĉ7^b)||U(Y+a$!tw C֜467P/+0M[;j(k὾:1)a %A*GyH!F cTf!|]7*+c7 =Get -ldL6ovتt`oTU#skFhV2"XL Fd Fc,$ˀK} 2[mtM=FbSyAZeSbn,S=FA 8C̉@?T=sxˌ 2|YfSuYʽ|/lje02 Ip«S/B^x(EByةQzSk5/O tTʍBΥnҐolP"qlvݠ[V[@ztHkTqB*@} t9paᬽo\X1t քV7n´ep߰<ȳfjkI00 z4)$M1 c"r6JM[?呂vdߨ#pY"%r%Kc?.Nmsn;U"mȾp#mn~fA;92>h9""JJP>^},;/ S,Gf(,"h`<Ցdw]x酮m[n_36j.ز%ӮpbQ箸uOW>hpCrys50m]M9;yy5+ʈ3Ƶ`,V`ҵnZc:Beh n2-ՖS&l0B띃' drJXkBMME4*9av;Kxph^j``#wW(h06q 2SXBc'M7:jYhi &ڋ8|ftr`Jnz%j5A);X,+{^wql+,JeqO죨DtoMUQ&+Bpx{rKm։ylR&(%Abht㐤q<*ma+wW;͝q; A-<$eqEax@M'SM+06ԲC'sb~uƒvd*f\U<'NE)<9q5Hq>$N hϟK~B)P*[G [@ְ !YJaIHhԃ1vx~wg #C'j6l/ߜkz^ šYdYM%tK 6paD ksFd9z^w-l>I20hEG{gGF!wNG'K+<{2/J{wKu6Z$OF+q m&-r@t`?#6{ xտ8_O.K0P .g1p9Zή?CS3Y+l,) U[Dp_?OqRNroCG/ι HJ$?*9eǗy$~%9&'p̼ 5zSqtMϽ=~;}V<73<$  =!qAq6aU|/Pʽ E$a I#"u|!>}gye_[ ix /itoFж{2+`ĺ4Z堞~?>w%8 >v5{1r=KخE<,̗ {ʞ "3g\5Ó vXc,_ea:?j endstream endobj 1029 0 obj << /Length 1012 /Filter /FlateDecode >> stream xZn@}WxJ$2$ m@qJ{Ɨq+RN3dz'Uٸ7z$s4l)ce=@VUi0?_LW JC"zO0 G|ab4gYI,M1<#kxtY]r"iTzlI0{tV+?@ӳ7yoCenk8gC]G+]ּ]32f @Ч0xT̓iA1 d/ZJ@\@x"3UǤR2ˀ}$ oڠ{.STSPQ,k| 5AlcJIҩ ?srNkhAJlrY8K??0pG<6kaiˊ&2 s=;ɕA;NÓc%ѓXd|[qrJA䀞eCz( Ie~ӯ&S?M 48ՠ`zJG*1SA1ϳk2ui-hkx*ZjJKPkjLw)o4Pǡ We< sl)%^҅V7RJއ˽p FQ首OfEkj-UһmvoU ѡl-JSK٪pۏ}Ԯ$V<ܖ~>t6ث1B1c hn(bZkFȼQYPV O }F W*(jҔ8*yqT NݯZ:ݽ8jR7KemG/=GǾK3˺R~:^RըR'IwU;?I:i-'I=4>9ŹT ͭ_@]πРoV&2TUeK_ endstream endobj 949 0 obj << /Type /ObjStm /N 100 /First 907 /Length 1670 /Filter /FlateDecode >> stream xYn6}W)P 0Z S\"n{FޕZ(`C#C!T Nsq]5ǑpͮH+jWv!8'-X-j\Q)#FAQ$L, 0:Y9;dS5Ha0$ٌkOFͰ F-揭C.01.X s]r3.ٚۓjFuQ  4YV0SAs(KD(H4R"(xCad)G  G\%!i"#L&j30_nfZ*.[\ ..Af3&E.E*0*Ƴ=1T5 th&r p 56 , |䰺dV^AaU\2\1xT`4֔LAi.`n옒r .2³m)7RB/K5Zg t,I6-@5D%FФ}N53"kO ,<ŇgÇxڸ#w1$n{@Q&: ˛*o]bN]k_.lܕߗ|Zūw}1,VMby}qfah/FX-x7'lqV5:rr]\w=xY7y"?'ؠ`6>gH+oL<G'ir}Fz șe$BBi34l9EGk oү{j 1 bJ`_!\ 61zEtk~Kbe0Ys˺N)g*Oc*`C*Ҟttv1BHCHꭜ[]\c JO 7qP[]) *&>']iTs>4Оxf|m?^>W|zڿoV뷋de>ĥ 0.Yk45󞝱 us ruv"Ȱ<&c]]1% EdhL*:#N{Rےτ3om}U m0uSrv_7]ӒLK(NB" \ĥ=t9+ X>~kPM#׊NB\98K?d9^0sS El5B> stream x[Ys6~ׯ`RiƂq43IZ}J2Jm5PRL%NXG~:3d$YhFܰt}/`HI˹MP|&l ޗpDRkGǂр?ˢcj֞iAʇq?N y:N0$u2#y6x2yCWF" $iWBn:fvIS!@ꨩ  c{(qq8_7)!]j@1gˠr1 S">p6D8oe4ـ$ Y1)Fl_p7q/ &'6[LB0@A9 HmP{&&7y1h((Awc-cƁ6Oh0Tc<9~2CɆk4 (Rus; oA-iT y#R٢48K,6^YpKqN^|I>/[jy4PՌ\sŐQ:bU(wym׫$S'|Z@zz @TK~݉5о؝(ň]<7ƌ G ݃zK00ֳvf wS= ( +&#_r?#%IڃlQH p!禈Һك#p/|:m@`r G}ZUE} Q}F0GA0uã ˫|f7a@͸.p2cc&.YirLjLЎ|8 2d(&MOjk^+݇2-L&1-sL'":7nHU xtnۮL"2}l4)[bv`#QP#ՓGtFJ `<.IԭڰY2t VvW aCp혀 HhYd(_;`8RJT,prHFwϓ*` !jVu`K!kQ9Y_eq2IMn-2l4CzG˅GKcfH߱E=w_JGj\?x"?Ke7;w=WꧨP=ժ~ΈA'ViU"?sJ,R1 }w%mrAmorWSĘhRNl,(X>cR$ņt,8Mi v΅ ܝne|EpDdi|/4JH8`H" __h0v$"T/=t3[G2֍=0)r4 endstream endobj 1040 0 obj << /Length 1634 /Filter /FlateDecode >> stream xZ[o6~ ]da]ۭfO]Q(6T4ݯߡ$;\LWb?\C Ds{̠M*\= TFW\ujT!!&vi$ Ba=4)43PRY.efFT ̛xTVzΞF@F!U@F `ۢH L0"F7VZ"T OHsS6I?v)) EL\ +$dgSOr(eq.%~.vy;)Hzwh^+&dW!Ef{[Wo߈!y PC@R;YG$k%CR+A{ȸAP37$yAVB?zZ<4MހehN1όVtH/MVq3haq c ty%. Kڣo8G,]Q2עa|ZvOKtU7{Y=96X?,GbےξԚ^V5$w |kY 6˛LS-0MSF%(ل3#"f EZq}.Fz:.ЍU/,PBnLfF {^,/ZH(-51p68_k~֙eV=ړjG0 l|0P8o0v剶? z`~ras*& gisFH˺e UjVt GiѢ9` !&o?\MM|`5ٛ :7/R܆3 )AA~I)+}&&'&UIP-^q25VHh!Hb͌ Dޒn1%~Ӯ"~Byu=䱜*HD>r j,nyn!:횅Ǵ (>,@ vݲq(fQE0.m4xXuySDHs(DzحQ:r0FMFtkKNo endstream endobj 1045 0 obj << /Length 2079 /Filter /FlateDecode >> stream xZ{oߟ8 T}k.i׳" JZ[l)RLJ.)lR\kgg~3$=}{kkˀPxWJ gjgrPvILTH@Ͼ8pƀ%-O}o;m`{zΓ:k❟:1i[~ٛ߶ IS H(ܷZL]giـήlgs5p#5Xj= <{st epQ谶K_>!7}Tk B9i'U-Q䭳Mա&L_Ce֐ iH%@WtJP_jQO +_ye^ V~^с_F<`TMa$TjeɶS![[w{3Tj2{1bBWYW~եq1Ŝ]UvBetu9[CKwEaH뇫wC6 %a14 !?7|H-js=(C,ӏBxʦg]Rn0Dq$|TH$0ֵz1cqn kf]j8X{i ,!}o! |dZW{ d8 l!J2\ҙ 0Nc@-V3 R ջz#hh_ۨwطV*NK>~ X,ndS:}>*PAFpI{`Q㨾`Dv.Y*Lo @0ؖ]x u?h6AiE+X("~캺G@ƻI"]י.!;5}"Eh _kvd_=єbz L13uuYnY^eْX/QI> )D xpw!0K\A+IpT .PKkbx``wӬ[q?smՓͷ?g ~$)y%Et`a٫m#^mzi2_h'լP[]]bmh+qiRY endstream endobj 1049 0 obj << /Length 1829 /Filter /FlateDecode >> stream xZ]o6} 7C@> ma6h%WMw)Ҳ$k"i{/9.===?c P .V q<}],7㋉L.'S\뤘L8,4yd_X_x1baD`<OQH;- |zjq{=c\qu/ūj4 &fHF*XEQR%LR*Kj QZ%4-j {qz L,"Ni}rݵcզ@ň.)[^9Hp $"̷6y -<Çd\!Pr?T1uF(r+3vl[y @L)oJ t>'u67`$zQ}{I{U)"]ܦ> mRc" v3DH< -O 8RJԊ%6]bg{ s_^N'L? 3(Y8+ރMd +t!Rl2*qX0#x1!b|B#m)RT-l[3%z y8-*-pp&rze ˸h` U>Xci=6(VN00 s!'fj<Ҥkr?0_qL%!(h=XT/Hp汎 We~i8՘zk H0!M9˹1'ɘٷbO$S`@#陵A6 ;:HHv8F'|fgDۂp 0v>Dm3̤*2YC+G In]_G'qh%>V1T)% IpW2P ~moqSP=)]ɝNCtj![J1 ˀt4Q gA_$ \*MH LT8BRl F8E4tR?Qԡ¶qI{+ RwC351> stream xZ[o6~ϯ !)uzI]%!-EVlt^$rd:\hzR'BO}gx XX:ÑsŸ?A{[&iZ"+=~iSờΗC4i8tvFȍBZ:;;`#&FÈg?ZǽWf Hdc(/@EsP@n!OC! d׾%*\ C~D,[A'[\C֊wgo޾:[} >$S&tm,$9ӼϑlS;p67N4Es&_8ͽ#vUD-dVͻF3Ӱ]FH])Y"P"]='\eu!"J<ܒfp``F̪U煺:.j&#(d?'}.JQF=BYTu( |,,tq-UH(Y,OTg$q^cn%32wǿԯU/fᙩRbI7Y_*ۄ[>6:io*[ Cp-+\[52[=2}YCE^X=[ WUktG ߨ]V=pW %X,? qD&씤_e<[D&u9X=8_d˭_=0 94, LP<] nE%1(cCAգh `xu(FFE&*|.1Y'`̸Va櫪iYkz9xҜZ[GEqFm臽7x֗C>f6{>|phP3\[V4_x1r5[? b endstream endobj 1057 0 obj << /Length 1992 /Filter /FlateDecode >> stream x\mo:_5/\@`@W0ДVMJqMAdxn||}|yuna!A`_D"}#P͑] {'_57Fs]S.\'=ltoѹ W_R+g9s0QVp!u-`dSG[ < duS]EME8Ǧ?W5Ԏ K]X[&pU#@s0fY#0#6EQg+3: 6"C8_2N}O4z<;N -PpfeZ)l rL>Z$4?)rhSnXq)Lb"lGv(K+bǰ9dqEE.~zǙ%!8CrM!aq 29bzPjpQ RKPhs1j{&Og#qrKɅm&Z FYEaɣcu8^QLPd;Ax-Q X^ B`AB0,}G=?RN[nL\O)+t.G  Oub:xͽpf1j0 l1vxy룿|yZUWC[ `=yB4˺ʲQa(O҅r@ËS8 Ao#! S˯n_w ɺʿvxk_žs *5e7v :ׅTy,:W2|G?5R[Z&vk\(7֠Z5vFZ"V\(-Kd"Y{CVʒ 2de6rlj__ @` 6`Mg%nD(HxNuxy~yWv'4N)N*n)ϑM/_0[\M6E$C%_MF6jc⺈z,8S{'2np`|iqG1*KBCq3GFJq{H%!s'D%v2!Y i.&z"%S԰iF$#䭻N:ֻos l'X;D6ŭ0&i}]1g ~RD+@)n}ko' #QQcX&ꃺ/Ԃ`J0g̀S}&]^aa- 3xs'@sja=QS:ZBx'+cqw^l0u튛ɧe&RqGS׵1W){ >E+74M;~sṆWuhcҲAjgGU!;,̢pƓqrUX`|D܎Zz^weg/OTʌjԥҌjF=$n_ {)T w*ϖqQFokJSƕ`vk:iWeJT[!{:e f3,)8ݍi;SN!ݱ:1<2},Ni! endstream endobj 1062 0 obj << /Length 1860 /Filter /FlateDecode >> stream xZmo6_* 1ڥRb˱[rw)R6LQ!;a֫aoQ+@N,Z#7`pl}LN>|tTΣp8MTQ4 _/=Ca(.g5F,𭋪rיGk5q,;ѫՓj4<x ,@-`$fssDVkˬ%r S- ,6,ەb~Z$G< h@Ban2S0̣, 1"4no㩵OH^``> <ı6p׬;y9E .M#Ԯ̡;{oߩ O}MG@CѠwP'uyywu7_Ep$8@>wG (GhQgD n.FP(ρ|4E]5fH#͐&R]2Dmtj^D~y@}C0R~Q(U50EYו<,S'Xztnֳ`iRDIњi_S/#=n#fni]I8G*)jdF*W+qUjU=g5KVWpET{k?'v)"D\!b.mUl%n`) X6s;W'U;t9`oi2[}Vfxs endstream endobj 1068 0 obj << /Length 2402 /Filter /FlateDecode >> stream x[{o6?BâIn hn6 +K^=ͷP$eKDrChj8oTs`./ԉP$p./p,D,˅{-ڛ tZf7n\%yJKo ~ˋq  `߱(tͬDߩ/xWhh6ݼ8vq/U)+}Qg"yz$O*yɹyl-2(wF4[\; }qt!L+(`J'j>!|)MR9FJ#>S |2ia-q;#@({yjF}0IxƋO8mWa+Oգ*no=3 @Xc|`rjgLۭ}Hw($tolAŽg>"a80!.` ނ g!q' Cm-5 z 0Wz( 6m]+wzngL[XVqܪ[ncb+"w{}jٰ[(z;-s5KUQGDa%M4kM9"\QF( . BaPg1 @[#۫]W-h U^VÖ'4@NDvC<8#l~i&#nB8>ߛHm, BKeCff՞i'϶s H 0 Fаer,PHWR8ad+UZ5!UNz=P .{ =(M٪KQ_KU(h=m5t}(rd^J Kͤh~{/%ڭSҪeG/ZU:bGu@o p]H+!ǐ}7Gʬ*Sh|7}b쮪jSNz@Tz|US}9[pL@]:f֦ipC5,U@:߳?kˆʯB.UiUIͶjU R*1?(U-7 i-љS:)5<^>lmSޤ0xo/0Иi au'{ِBA{m|Z$| < s7Y_dh!\g [Op[m{EIxh817Xr4^1(ߚgC˿dBG$ nw^m[ Lǘ@Еq6X@}UJ\ʲ2Y}yr|Hf(8QJ~>sڳߩ8}?j$mmj<Om6h0D1 Gwf!T¦'іgjkSB!Lh,%@V}QNU͗?sI30g̶NIg6;H*{nM=KreSE{,Ykyp,iM,<1`{j 4ى!raC7C|ݲՇӉ…zНJg7!ؤ^yUG4ߥ1_]bbsdX{.!J`8C%l^<7%<ߜ{}(q<gh_ vw&>ՙCt)?՘.c^(Ih.(&WUl{-uWa-/RQPsO?INҠ7,reLԄ&3Ύm4 endstream endobj 1073 0 obj << /Length 1598 /Filter /FlateDecode >> stream x[m8_Vj%[l[$$; *n7MJ^~c۔$]A8n2όx2[x{1m28` P &sOROrx{3?̊x1S'b|4a%Z/ui{CBxnrdq@`(K#7] ޼ _z@yʧV K_o³/6'Q 11,@B)OvoIGqg4Y:#\O0=^#(z ,*-=M"HEs e-=H:|?D2:}✧٬v*)$}nڗ }UCW6#qci40q#E:Zx'o,V6g$;KVz 젍m5y:cϡs,a_A@-7)*o ol- 3DLV]bDp/ .9e.b` HS&@Д I&՝o5#?Wnݝ Pp:# 5|~GW?Fc?F05WV Ƴ$,?0DWK{ x_馴`zfeK endstream endobj 1077 0 obj << /Length 1632 /Filter /FlateDecode >> stream xZmoHίHf׮ztmu)*2dILlfk@"Xݝ"xggfgy.1(TIB@Û?(/AHU+/zV BO4K Aƒ]x8 #C8F,iesS;]q[q؇_j+h(+LHFQF*k,8Ulo@I"ڧyT|QִhFj; e&HExs@sV_F$|pQknt>|mfcbjTȺ$/8/tSB-tY.1:` BBP,S!7DQap̊j(p[~XYϹ` l&38K}vsGdǍfo|¸ȣkHE _IX XJvElۮgW{wZ>`!zg*/>!Nҩ.O'nlcT ¶iyEc܍jfVtJ;}f,Ց0 S60(ۃo!z ES⎎4fw\!5E Q?N9PCB( E>JFiV? {ϧژߙFSFu!#D&u}t*;R4s{j^b៥ߵ'Yz}sl'9' `MwSG{AuFqf*hgĹ / ’4#뇃cV mYssW6`86%~L$,VH>R"0Ol9IJD  2_m}9da̐ !t{1V[_-IT=%c)[[vH[ZAt?m17/wN>#bqn\X_"I] ʖAi` K !Jųڢ[W6rm>hݱ Á]S&ie§~a@3kt韲O /C"T0T8uJ!jO2S~q" ]'tvus}!-Gu7ǮuᮂqduMZ8ruWpDޗy/[6%e.d.`jM*L?RZ endstream endobj 1084 0 obj << /Length 2432 /Filter /FlateDecode >> stream xڥYmܸ v[~O r]xl͎z~XmrHy'H,v,SICJ/^Hz(Rz/^".",=d/jjU7ugը(z E./?M/$ֱȂ̫ڛ^ @DE=Q9<O7RMY\"2R; $?ˑZ@y2rP+ﶻ PVpi!.7qZD TRlSO,aWi6B!u޻-sa0#U.+$[Mp@W(h52)R4?Г<- |I/Axn%vQ&iqh`4`pkY"ڥhVx݀"dЏ0Z/srf[.a9e]sJn"}~) 2Mhx83vvfG`81PVXrA}AԆzYC?D-lqp':9ݕaa[LȐ3i BPuG&N69mÎO [l ~ JR7fq#y7) (֬Y'w~  5;uU:~eia$,r mFBT#eMKc&C8ZƢ#i>o+mM#Xɥ+jouh`anC 5ץ }zaopSCB[w2 #8}szxʁ<2wq$@bueO;([OG)L|ݼtDds!rU"ɩ$4s|+rYc秒U# @xsK"!??'Amע9 }PKxv%j$c۬fy|GYeeϱo>&~i>P#b󸆿VÌ6{"{~Vѡv^t0;ӣY8έ8c!79V{xE%qSX9L+=1اF"'7e rN@e#̊c55B7yp0 N#, tRH4߸c lC۪O)Xi Բpr K0:CPx .qy0MOf>h30C㬕vGhUt"9e CmOa" riކo[לyXսoZ Ueg( Pa ;^4B >[iV݅>7;VN=ß|[汹gr_(mY_k_"f]GG"P$ #OBXdY2zH} /~XbyB8u,o1Og]&5^8~YE/a2,ƗCH2wI, +u_sdɦRSo)djQ_;>R9R"h[3/,%1t_aiW,Eri<%S,ƅ/AˉWfmCagqF, endstream endobj 1090 0 obj << /Length 294 /Filter /FlateDecode >> stream xڍOO1sl[oH

uڗɹTw= p6pe9wќh_a8u~q endstream endobj 1081 0 obj << /Type /XObject /Subtype /Image /Width 458 /Height 627 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 46886 /Filter/FlateDecode /DecodeParms<> >> stream xw\Vzo" Q,g콀grSφz (ͮtv-3?e,EG3yKfd,?Q0 iI"P?z7+ U)++%bc^|1''U;iJX/N!(oTWthD"u&9jt%I͛]ݺժ N {EZ^\ҢkoDc2226mҤi3sqvU57{-[\IUxe,QI-Sѣ/KOO7334h4(m[buy, gϽzrRRvԔ'kkkjл= <\NNgzz]t)331>}Qdzn<ʪ4cJl??/baCAwةFۻwwMM>[0 #Ir=zzy%]x.>fffCwuB>o?455ono_J2/22 zyݲhђׯEGGa޼y)SiLQTb|2xF|gL[rՙ珟8ǽKN?*`"]SK,+99 LMaG9sǢB_VXF̊rvn|8vj"/\8Ot$/^@IO<),,6}]3inn1g|eTW 2L(f47`2YHe,,阩PVՍ#JJVSm5bU[HKKhPWנN599ׯ]CNNAQR MQQRy-,bc/[fMlm~KWWO0ǥc uJ$ItkP?55u۷0Rd2GwE d21jR˓_bUǔׯ]~0೧A`eea9RmHe| -Wܤ x"]IMM1OCCS< N Bi4jԨc$Cn+Pb =ڪD bcc H$iS':wvui%Ig$))L^ ܄O8IZtz&q||1)ès-p8 \o|YOOO>/,%.((峌m w$33?}553Սn/ML茊qqYH-..yFq*8066F^dSQئE$33333Ħcl( %}]::]moX%Q`ӄl3]Z]Ri)L^S33iԜ:u>jbӄ*Hm/,%nAA2Җ0q2aaѰwow$&&]2j_& ?odae$&2+nRWxaPXX i||<}eQ#ټe^te(Q(Yb=OfPw bX, j[X@xx(ggg޼q]ZJD"D"znn7™'&[XPR i+p' bQ@?tu&- ǝ`=f >;~t{_C##$ɕ+MOˆ=1A&^]]Mq&'} -Z)S ={a zIUw>L&Ν)))L&ߺy)nRW$D;a%~P< EWnggDϦ {􌊊Mz߃566ջS'h)Evw(##CGG?Gn^ݟ> vJ~~s:Ul _=MM-JK֍۷m۶eTTnsԘ ZW޽ѳT߾}аi*3Lдl0etnss)S5MΙ3S'󍌌{]&Up˼7V-Ber iCj1F"\-\ȪQ#3Yں15=hj@a*B TDj1G߯^|̙|m,1hUSZIrU7^2b?\U~ `iYeB #jc˗6mbūB_,bȐa zs8_0M_OOJgȤ̣ڼ=L;eoX%Lmgҡ? >)d2\pGUfB9}P(V-\8ѹo QNCYbdQa$  gۆeQ *?5BuJR?C233 H&E׎6tԹrU}B4ggǸ>ۡtdA^6WFB<ϴ#qbX8 P q )a=qL) DgU__mё_ Eotw޸qϟMLLz/3fڵ#88T{ZCCqy..m*e2QR;vt '7 =+ bޤx` `őKgs$P$)Pà9"zEF}~kK;zdt*5s=o&7pw;`7lp鬬sgL<Ҝ^$}u} ܱBMFЫ@DQ(.fPhi BHaa MMB\ I4qˎ @AA!Kg͊2UXh֓?QKKϦO$I.~tuuqq|F__ ugׯ?+44dΜYOG=h$;ul7n\>|>>#Ck {MgK;%iEFKg5l'6'嫵Mqwg`L"KìɦD"WS;Pkc&\p{zRiH 0N8SqA=755vvOY3@oW#Hbcc֮]f={O;uLԩS" akAA! #Y_[aL+Ar[,J\sFֽ LbND,[B5HQ:'6}&?~tӦ- 0A1 􈪴c->CN4$a)trry]NxGgq-۶u#I&] ?; Xb Hn{wN50TGN><;FlmL$\L r׶S*IVnSOfRdjڴgA,K"wСx[tI^MÇ|?~}eǏڵ+UdBl֬gAݻϞۗ)NVK' lᄺ KXaaꍸ-撚-\\ssyR844ƾ/02dc,kQ{aWWFPUk֟#G;vb;AFJ.ZU+׭ K޼yu`gSi<<lذnE8 I`0Gݰa*6wEDll̿kWqKEN6}"Zr_{ee(p&X)@jj|pL'"SI1S$V,sl1^201&q1 d"U:Ƚ^UZKNv6.auuM751?tA8;xz{/Jֵېp###*&55SǶae 8trZGۛoaa91"SC]KHz6q1`I~/D-^A\6%䀄V@9<#\8za/e(xFS5ut a", 6@J  gX7,<\"YCLl&Rw+V- IRSp B[ ɬl2O \ha:ژ:f .`8$e@  fI~Q^$r L hoƒ7 ;#H p 88K&'M\`fu%$d@ {-2:2,٘|k  E!HO#]qLnG_j2E zE]*mfEPNP_S1hʕ|c~s@ˆFڒf>_1=3Ç"B w)@+nvCl&!%YZDHLjv6ðZҚ]^pA<4Ux_{Kldߠ}XSTϣVWM. R{\,蚚@߉Yp6@054jچ$I @ *yŚF#IB0̙T6~#H$"u瑚F{0{E Us5mC "PgP+Pݽ+Pݽ8s@_O 0L cΝ;`fq}̬sgq'k׾3xeS U5k9WĝvߘX, ;wX$^diۦB/NJyfp(/8B. (^KJ-`0;0/nTə3>,klcfҤ)ʬ+F&f.@7)٥Pa}K.޳a>ǎmİK'K=~SV&m8;v@_GT5Y@ H'ͼA?~O/ sSK qƤ/${[fF[:u:J#i˻603nde1aXi;Kk/_>>Qn]^R)C@_رm88;=sm@w @#gZm׭MM$I;wl`l\۶ntʕ˻w<ظD_Cw5!I[g6Q>YE'ޱ}n {ڣ5kYȘаw -&uӦߵϟkB11N۵s#I2=#3];ouּlqc1 [lvJVE/v2޽Pl\Ûܺu\f EÎjnn#F0LL3gtO0 wjv{*{Hx֟=L^jd bݻgɒ)NNt=ܖ.]֣ԩ}M?޽z,{[7Jaa)zdm߮ͻO^4k֌egg7m( ;1[&}ɓ=lffgׯ?244dΜYOGc u> ttq,M_,F(P+ݺy;w(&p;t1"g6qt4Rwݽ_PPŋS&O2.ZU+_zUXXq?GP"33AAAҪc\A1ѣGyyy6*Wʼ j'mo߼/_2`8&լϲƠA\WWVƅaay/<덽ұ%7sƴx s8&&4ޔ8}5O$K2k6lӧ hSAE>0@Qn'ԥK1GǛ31  I_]kK2% 4|iZKu`˖-.]ٴi)1bϞ=}vҤIOT2ƍ̥K5nja5i$) ٻƈ "== ]@AԴ r=C|Ycp 7XL͘T"sڽ-t4wvn+/e| ~/* #pa~sn^[{՚8w(2;V3k:v1bDζmhytNK  CCxA=0M48p,&=({}uNN~vL榣S FV 6T6%[fVƺ}jeI2Y|qTO"]M.nc̲T*Xi=IfT{1IPWjYWٻW H$DNq1do~ׯX,hf|Μ9t_,^f/=#EƍmgKԴQ Y֭[zĉ߼yty H2*im+[+=CZƧPWj,0- /KXL΂3挾;YTtRɕe?85$qwP9`2fzTz^:1>}E/_dKs/\Ƚp۵j=zPC6ff |6o|ҥK|6ois?۷o?xR1[>~iSVV6++f+{!(e cPG~N,KJϓ ,\*I`1Yl k&#nqlv{_s4h?e*Sa8<(IKՏі 33# * \Qii %m664Хum{^~^1g'}=Aۧ_>T8,,L_O٩Q ..nҤ v6FzvvMN3Zϋ вQn]_JJ 9x𠷷7͖3g-pB׮]MMM6l8nܸT:͑#G;&4ѧO ְaC]^|ݾ}K.&&&'N(`===Zӧi]Y===\tѣmGEV@e.Ӈwjb![ő4oĨAsk}/O' $,XuoI$J$*';KzH$n'DF\q ǰIN?tA=urܮsI.lqL Y3ekK5 2ضh~}ŋc! M&''-]zq?zS~  `0ކEXa[ /_| /^<ĉ]m΄ǏzqwJdG?H˯ߢ[r8qsgXU?Ͼ~} vwڵaÆׯ_kii͛7?~Νݷo߶m.]Thܸq={ ٲe˵kרx// Ĝ:uj׮]o.ͤݻwn޼֭[H& \re݇޽[>Z+H,!y8lÖDVN.Wlgme2f1f^$;O'-455W^ [nV`UePrrrmۖb^ѣGŅbieb=Jo۶mǎ}eXذaCi&899X,gggݻwG\T`tv'''\ j _|ɷ$.W +<ȹ,3 4; $;>U'K%^|<4:E]ܫZokOU#k[qG9 GD)c,=pNSs mEڥb!I$A@"岹Sx])Ο01ODgW:MfVvak׮>qتU+N9G'k&>,, [N@hh~]te2'wNFHwСÂ_`2]tRj*$${o55&Mvs@H[ܩt1ҧ2VUoõJK`llLk0,??:S˖-iQ˖-?|PhԨQݺuӧcǎMMM4aaa&L.,E޽O2͛͛Z+4%HM00FjL+"@H@j6\T_7[M. iyLfL.XUO~'3La0"RL;ж pS\㼀o}qOc1 bOq-B5L%#xeH22*P/kO?qt|gkkk;;|?f8$O<mvE{;5)/~@' '+nիW{VZ!aaeD˗/4hгgׯ_?u3fAJHAJ]fUAY,Qvf&I@Pin)c` kثn49NE5kBBBBh|@eVx<ϠTeAQZZ\ɘҾ ??6"kח# d.䀢# uKN9|+8ur ՃnhE͛ʼngϖ'33sժзo?-Z?V^ qot{ɓ40wvqqqicfSb[GK[)Ѳ+l{!Ξ=ާ * Vnnnݺu>|Çsss%IDD͛鞝4tĉW\&HV\9y2E&L O>͛~H$={6iҤ +WʧM&c볲$۷o̙CNbŊ+WL@֊5b3150%O-:G;@3%KL iȤ $ 2nVQ? wAG:@ZH?5S޷$IhGBe3>IVެ za28U^_<}Z:G-aff6ceW*H߭{l޼888\pY[B}A<<<qvv-X}ysggee :E3ڵkm[71ЃuyUU^w2oŋmرc ,ٳg^[ ZPP0gΜDsss//AQ &Nw޷o߲m̙)))'O^f ]|@f Z #B/406`Z>I-2!GYY8 40]"i  %(~ 2H|P(6Ɔe(ߚ?y0 &?g+ؔVyt]ڟ`0cc6nڼqfd;vZ''Δ˪ L{Kr||cǎ;DzѣGxСCJ)WZDw3f̘1cpDDDtvŵV 3Lvws[Yml8[Gy"Cb dJL`2cڵ':OsJM@A h_. KJ|+q> B Dݮ5={y,--?dɒ#GxkK̀L$KO-*#0+amR0 H Xe|B'6L-@@)+sAQ1͌XR$Xv>#wM-1  4TFcƌIHH0559r3jCRܜcZ#=l܀1HL}K73g?3,dž5ͮU7^D?5t QxJō򅌈pWG>-G1Ƚ"AwͪJj9rȑ#Ę*!t2⒲ZYkYLJ*N.0s,NƦ ⸤:¾NLMM-U֗$  x@@Bh0] @jDӡԑa>GVM̿tOo/,'~7 ~-^mUe^y#J^)K#,C \:\kpq .58X6_Kp|]C-55"  Wt {C2 @-EZ7b$H=*[^G[/{0ˆ;'9!Qى&LC=u !IʏJg5:Zjijj)s}gϙ_%_W,ACBHLtm4ssK?k H 2䩼Z$A]Sr@A,` A!#*ID#(kqk39~ z=>@QaY-ZZح`]]q|x :jCӁp~m_ȩ ܂|Q_٬[ʱJ$ H&$N$HL\;WRrpܻwWMR_ػwo+G _afcؙ3Ss1b2LLWip5556]1[S/ag-?<;߻D t}(K=x/ !<<4U۠+BYZ<23W%Dq{ pu"02fm͐{] q۠acnkhW^I4Zi_tѭUCǦÏ}=mNܳg7 sSC](>p]:::8uÆX54751zj!Q$A 2Ғ#<;wpokڨתg|6%y5ol-QxoOȖV9{ظ3gܵQcFޫoݺڵX9lnL_ރ +#u9Y:*YD啨P48PAnN#Gmmp[h@Ͼ}nݼk=E7/}@\\\SUQ!*aٍm[,X 'ee^?;o{=3D$L,vom '2eЧbZEM_UDGvxẹ|HgR{0 I6ú 88:2nh/zԘ4y[WW.mtڵAr؍@T/Fxr|rr2l{zHp }uұLwx qV]Csʼ5h?' 7L/QaJUHP%:_`0%D&dfуu6\x;ر F TCCq%&x l;~ڋCۢuђ1NVgsXtyNݤgm|k}N{w޻5`C-qbT0 #Ie}{֭CCCJ:::5{Α#>쳩 D T@YyC*e~75jbൊ6sVifF*x!<==sX4eZ^^mi_of (艒vҿWXG#DÆ~a~~~^^^ࣇVVTh4Q9b}& a=>*g~졊gjnl6:% 3EQbUt(" (^jTg*!*m=“)mFeX̵RU̘>@W]Yߵkl[5޺e)S)Ѵ3nlʾ;yte+@S]zIXq^rQ[π -~/1/AH$b>ELX  Hy$G 0hT QiKP3*\+W^t#Lbԩ'ܸ^uظs/4iREB Pe3=N+1 /D"fwy9tԗǹ8_>/ߎdS'oTLˋfx8Hp%~P3E>"y.X@`z QHϦ=CN?Iث'2Np3^>c~Tԅ/d=ypɃ4y0lY ]zRLq08  4k{qQx D|2> Q`fs L[t4s(-#z1ԢbV6\v/`ߛ]:vWS6zrԵu9uڂ)y߿o9+v(YL8   @ (L=S]BE%r`3ifE $\.YT$kbEAz W} Dhs+OҧQ-ߪd)-2 nr\Qª<@ ]TcH$DnQkȩ۲XO}нp+ vt.(`!rJTX|H j@JdEѩG>jSq& 3ZIjz23OTX^ȒҖX BefEhp.}"Ҷm٣3rh~ceJ{`Se*TPEe~KeDu 6tђ.J`Τ6V|Ík"D]WP Ƚ"J@VfսoE ~ {ET1"½֞DA)S0׷iӦl6رcҔOOOuuu///NӤI6ݤICC|Q6ݴiS___iэ78NF?:Ǐ}p8...ϟ ?~ݿ?u~Pԋk-yXݼyӧsssϜ9vګWnҮ]̷ox%K(ɓ7n .Ȩ 9s&e,,D c<>ubz7 xP 0CN- J8P~߾}m۶eڵۻw-JHH?~<577߷oߥKEs}oߞfwa߾};v}E֭ pN>rJJ3h mmm6ݶm˗/KcE_M+P "uRtW"r%7 -w  Y}! `u}TX޲i.29RSS3 77$IPH<A|>_:DR>ڶmܹcmm}mGGǍѪU>|KƎ{9:R~qYAR^+YJ:r Jw+i,  pJX{T!J[^|).\H;88lذ!33S ܼys䢙3gWpp0fܒ,X ӓ^z…ϟ?/,,ׯh1wwY[[Kѣǃ͛GC077t( "uV"p9`@:jY`Qbcc---/_>d*ܹs>>>$I6klƍ\4cǎ=ztllҥKGUC1bDbbŋg(z%٣G-[,[˗Ak׎vs׺u^xp\]]\"ɓ'}^x ƎwzPBeV`cUidbb\+,`>4fWo.д %/^z?R7ރί(98tp_+KSDYj1}'&>ZZՈUu zvi  jPL(Ĝ/+M@@ȪP⫭z.,_^B% @ *WP Ƚ"J@@ TR^*12Jw ՃoЦ>zz Q @ * @jDMx6mp\+++=vCsy5ud'dA- Eq n-(uu*UZ |@GAu!NT, "8쐝ū=sW QkT?+J^nUΜ9coo=Tz]$% b￿G"̞=m=gffٝ9sv%!!A,}ouB;?tPnݐ wܱ=rH}ʕ+&&&gΜH$?ѣǹsӮ]x~Wiii* WznOEVmWZnݺ{ӧ]\\D"rt^bccx|ݵ3L&H'&&𪫕ch{{GAR)oŋ^^^$`L0!??j:))K.H Z[_&Z_ KkkgxնomA H+4 0vh#p$ @  , @ ^Y@1Ronm/h i4L*@4UWmTRPf"LLR,?Œjl<@2,n 0 TB!N'MF+Օt)/:ZxJ%-w!SHDN%01R1|K9Ϧ3pC#A@/0T_*c 03K PcTo*^I$R&/'˖:W+J=i3|f?i87rs2J$=JrqA66,T*'{Ӧ rūWxI ˗%q|75bxr*J( #&F,WU"BxdIT|PO%+a5RB}ZVRITU $''d&9uԊGK|2F3f̓'O.__STz.4H `LFPYXV'Obd\yݦ[0~h$b^hK]tnG2ᯝc:;U\3ܬ,1lӞS/ՈqSGL#dq@Q\LR"4p 5EEQFYX,dRFP!&c̐T*y pCd0nxFCtECR"4ghyhFEY`XHP aT,y4ݣؓ! 9!!/Ӈv߯څ fvi_9soi\ ='R1c0F$/<0IN^w]A?>y%\w:čXK'58j.\ JHHo^~adfB8p )JJJpԩ#G 01cFqq1b2d\Ej=\)/RQjJgʁfj}$?}~{jE/Ӽݿx%Ǿg_?̦{i`onK9ۚ ER ǙY^*Csf%ј4i\._|y^^^Ν׬9vX```TTZvrr駟ԩSy<޴i޽{gkkozҞ1y"!4.yjN $t<ͮ,O.|%r߻a5 ieW\Y?ߺ:VNJRjˮ: ynVP2Ȧ{O WXXwi/Ŷ|||RiqqšSN]&%ɣG̙ e2YAA?j2dܚ-xZ>OոýUFnֽ^+ܿ5(tiN%L='0Z",{t KX@ M څj)s_j_8:ak_w0)Ɍ[`j~1A3:}mì]{_ZMp @#_y_-)P(oK 6:ks~N-+fM =V*e__>ȷjf"mM*7/CmM-%В^ѽXRPȹWUsh~J/lY9_7s\vD^k5W4,Zu&)gϥ1{xlҠ7_ȢWa=fo}~{vY|AWlF]mlb>VEjg ;a@Q)vgU/nPշRX.KŁ/+i bX{~+ķ/Mgᵽz_H "U}|YPv hZ fY.g,d]@ D'D"$JI}A HsS^S.IeLs1S"a0i4P$VzLz @^y1e5ՓC\\HtppطoZް%Ϟ=?~<$HnnnǏGXHnݺ_@#Ip8N /F\@^>tr駟Y"8J$a$%%EDD>| ONN6r[?~7oAll'WΝ;wڵ|>bEEE?m}vi46*+Aja2z$ +رcϞ= "޳gOttڨucHoooA$=<P-Z:eOOQF}WHÇ}||kjs &ZDr5Y6[LS^~юM5k-?RSS/]믿XVTFL:@ӥKwz 3fU[gluvvtp}5JFFFj<{L!Q[lO>Hdtss{𡗗Wi|mC"`pppPPн{r{.]Ǝ\~Ν3g==7DB0%%{HCBB޽+JE"QjjE]@ y]㧮-saE5qF]l>P?=<_>x<>>>-/++2e N'...Gzwj/zyyH$1a„|GAR)oz:47b5"|Ѽy.\ЮH$̙*ҥK fј[[[ @B[޽ENNv=z,""͛ Eի?~ԮӫW/R+/_^hݿQ 1\՚2p$яg]GN81cƌw޽~έvOW\ b@cC48@z8///Я_:k7A1 Ǐصkq8_5r&[4˖-b6m*//͛X,z V~͛?|P^^qFmkIIIٳgk[p![__4p333@AAA=yX5E9">XleɓV'Na5g$(3g,fc5̘KXk}ks'@ m//Ç+{zzJҤ$=z׮]%%%nnnSN~llرcz0rT*9sf- bŊÇG1kK ?%%[[ec$o#&x˗X>{6䄇#M֬;'T__\͛f۷KׯB,NhvX EG˲q;\)8tHz:y2y0#qz43S-ּ& qC / &hС7nh[޽/R)/*Q@TxDgFYnZ&SD£GI.U xIV*ǓܽVu4ab}\xO4 1X!!YGNZtD6og:06kjXd[[[T ruu%L&sԩtiE /*:{x*SLVXvQQ=,,;Q!Yt3f۷cMMǏEg,:z"!hFÒ%gUN-AC"ŋ"""x<^jjjFFjܵk/,,d0 .E /Ym2P`Ȑ!ĉk6C_p8666B^k# % 1aÆ-[[s%++kk6 1?)]:4CϣGD^j[LoM"| b;4 yos˗/wҥK@.턤ptB%dhF "]ǚ"]+V8~xVV:"U_g V^;([CCC-Z& MBstA _0vD \4iRttt1cj&j0qqq<ΎF\5-^8<<̬K.ϟ?yWeԨQ[n]zŦMH~niBD>>>6@mѹkN.A'Ǐ?~|jhI;v숉)..ܹ˧LV-ɜC)H͛7?y͛7wp8_9s<==&ѹjo߾˖-CSU!=yù4{fOHرcǏA.],Y߽{WP:;;W'6))駟~z7o.\,,,vѣ###~~~8Ν;x}:gIUgPP22xTo*k2jϣ M/BU1[#A|z*XQc 1՛\O/lM4"yuA }G 4 33lllvڕ 5@6U M;^(HΏ_ȁx*A؏89\ZVT£B I!^%QU@T<䮄xH& jZJS'Qi6P]pzx~#ׯS(޽;y?iӦ]rǏd2FM Mltt!ϗ~!~ZU(k@T-rcXF™U4m.ϗ+RP!{&xHsõfNիΝ;رcرHw? 33!CZJOWK.믿V\rMMMW^ Slbͦ;`(ǽut`b(U2%P cuJ㡠2DY}9tqxh qo$[ҫͻwj<+[LlejDmR^gcWy!sƍ4>_܂0X a0nxFCt(VP}X*CĐ -((8 ,KAQ)4 !L%"@.^^^l6 'N8ui1nT"!`0/y;wj 28>>JUҬ,mV, 8: F9;Y*c9NYJnHl2 /弝<SFt,d+YRN8G }H .n/UVm޼ȑ#k׮mm3%(86h) eO4k"a~)bqfyR]%2%gVLDC;]_!(,kÆ O}V\9m4ԙuui͚53?;vxX,V*[lH$ŵA |)vm۶ٳg7[@ <IxxxllѣBMǎC^:ضm'F*))f0Ӛ Z[;rȂ {ĚKk+99ŋwiWHhZs׭[]L&۷f͚5b Iھ}{dddzzm}oi`xjm!ׯ_ڵkS ԗk{ hܴ] .>4vk˚e w1mTk l-/^nfsHdͲfkU@VL5˺|v9c!JJ :!D@$VzL 4-RlZZΝ;wİ۷7I6Bׯ_;v5*66T^K' @&gb䦤bnŇpN$JI}A &K.6r}ɓ'9::Μ9|ӧٳ{ƍ￵_~9`n"׬Yӯ_?@ޙ3g bkk;bĈSNi\97o_= 0D&@!dJRJϪK@:M%_k;r#VdggkVNIIYxqΝ5k?TC :" L4I./_w#qz43S-W@ MA+^˗@h ^L Tx~,:yRr.\{m-Ƅnm kvSP ߭# @t\-T,!I2QYTe(bL&eEQb; !K 2լPFܼY>og:0B 6MUtU8sseYiQQ _iMH?K*liVz@1P #\ERA HP^))└JhaC&ٓ'2tՐsr1 KxltD`xgL?OHs@ .<ڃvA)]:"u v6=ƦG(?|e~ ]l(r 3/yBjgВVItc Rщw}@ۮVF#T9=s7k 6a_0[^{HskŐdc| c16hD6EoT ݖM#9bҳiq?~uerC_!u`O/ccAY3'O$J}诶,#T(nj~_ʮE%i`xagy\ȣ)p21'e]z#_I2%~R<-\ufO>mI~G4XA!6F;^O FWkuc?wfpk5)BL]\Sf ^k=1I>{o *UD!z] iy]S\e>e0EL^)*kZ$bN[R+/%Rgl}^~ aRF cWK#mk;a;!ڔ$:~Eyk5w *6Yּhm: kU)E'EODg"͏Fp 5x1<GDJ;Օj@%Tpf8,х3jYU)}(j DD+k2o=6x}Ա'|K16˯!+?DEOOI)7_7?-`0ՑǪīo'v9WTNbzҧ@^4UiuCBFr%r-%xKgәLN8G㡑 I ϗc/K%JұD7 $Ws)͓epq+lc@fLnke';>r݈x쫲Bg@Ln7sWlr:nmepVgԕ`4 g\.#Iљ&ocuDM 骹 ܱZ&=jTeFu@t$w"&Tw|} |K9@^$shx $ ,[89£B$KxIDW!$w%TaPz_ PR4_ V[|Ю7W(pvIZvrq美刜-9:\hR>.\&MxuiABoiWQ@URebi~4 goJ|H(U( g2noyoך!ʙC[Xr̡& -3lrt Ek8XQ`eJ<ꌕCAej%щs1?s  {C?->'w^vr!䠶Z SD{#W\8~RYʙCF+)l3W+Gd:>=\f׳_o#"ޖFyhlb:dߢC NtCIj&k35?P)~/x31G«@UTԗgwoTճIXokZ =;0x=->qp ǒͦ3t55bv~c?Oi  1l[w>sk~|`KAoủ3b$K  ѹXAbX CHb3<4,p L,eEQx(d0 vX*K*Ji=l {,j@_@'8ȟE=9Y޿.ܪ3º7cݸu.us譿S?¾g_@<7 w ߽~\8vy;L1k] Z\w@"k|#-Q)98dOd'29z@ŕ3I{WͪI z@uR_Mpitf{Nu;]QZ Qn iC F"QSDOJlDMxB.k/s/ 8JI^ԝW?wY~1@: +iR/ܡc'S>L}3zǹ7Rblj()|ݶBCسe%<67,HyOgWñruQ~Tj kp&kgJEחO'.y]A$4Wwl sbAύ7MfaaA"{禁43#%aXal7B0nOL=oö%~3j=smaos>f_Qa:=gٓ3k+6#ֹ];woH'|?s:Y[j7ԝy0T6-0ѣ7mZPP8rȵk׶_ D'|U.@Vv횧'@ݻ#GbbbZ):0Dþ} 3==<{lL&D"?yb 0D"um===i4ɖ-[&bq-gQ jj !)))""B0)))<<<99ِgo޼'N@ʯ^:wܵk|uyUxx9RI5m6{lC@Z^;rj-vرgϞAٳ'::ڐQԺ1yII7 58pۛD"ȑ#k C !;&99ŋ5MHYKVWzBӧO~>|xnnnh"WW)Sxzz5ꫯB>|]S!$''[.--N7}@jnA[*jȚeeת 6~kurBѕ@tB$bnItW[G;6ՙjQ?KMMtүbŊUVT*@ h, ¾}"""lll ?H^ DZu)MjF.Cd1o'cMc%Ij 윑w!322j2q8Ϟ}vېWWWd6Z\\ܧO$2=|˫tؾ}{LLLzzz׮] ia^{/@Y @G *$j: $SUoUzVF,WU"B2U!0888((޽{r޽{AAAK.ELcǎ \.s̙3뀞ƍ?D"Pҽ{w|!!!wޕJ"(55u„ Ixxxllal|ԝ_ [=I= 1udfϞ-f͚+[[P4I޽{\r֬Y\O/$HC=I>jԨ[^*3$$DW: T[p8iV Gi^25\$S' fSK;Ǐ_k+//5':B֞R=&_@=P.hLIis=$HҍC4L&@$Ah}A H#GDr52G@O+=&I!;vdvvv?K.'Op8wwp4Ht@ MãG&Lѣ}ǧhWٶmϗ/_/- iزeD"ٲe ݻ+DEE9::9sZVW4<|0h ]z\ Q`x@ -D^!@}vCHv{#_ 0B !44D">|PP .:&uWket([\xo߾Ϸ_`vvD 6gX$ɶ."J&LԩS+**@YYY``UPPPee!살.z{WӑCw޽{Nf͒ C"ŋ"""x<^jjjFFjܵk/,,d0 .^"]nnnl6 u_ B@^eiӧc͕ee'Z 4??Pe99*mTRPf"LLR,?Œ>|Ռ]U\ԩ˗r;ii HRg6n$ѣBMǎCXXXl۶M;7`}߾}\teϞ=={ܹsQP."ص`^B MU,)HyI ֖lZ*ܾ/_bi4@Nx8b2[ARPT<]vXbN&LL޿^㩓&!&1$" @GF BZ-T*vWvi׶ꐶHU_ڢ%%J P(5ɤpCwEEREaLDj&ȸx{{R'^^ʨQ'7ɭBQgluqqIOO#Ұnu֥Kd2놴`*H=kb(-e)ʀR4ZTʊط/}qJ ?.Z>YGԉ2ly0qzzn C`hhEׯ_oذA{Iׯߖ-[)JJJ!;w*?ŋMD?;wnmt+gO&Qu- Μղ*+FAXӋ2A«i:7yPcɓwk3asҤIr|yyy;w^fM{rر(ZO?٪^]f qIUxeS^RaOSyZ.'CMfQQWrr?btʈ"$-qΖܨ(EB`/_v4@LC&NϟRl4+ =l { ?.:}1}Xnőճgiv«8%Ey[JhaC&ٓ''Ot )a4cHv-Nc sWHƆ@ f j[@ H#Wi`x@ MÛ7oߵkW TK.]txɓgΜY\\ܺ770B ! … ׮]=ztddv(jX[p}kWB$(3g,fci4gf\Ag3Y6&yxmnuŁF25˚2boau<֜2m%:MZ:aef5I$(33V-L, \´8ha}ꘕzVͬN[Y0gԠ%hm%''h1c>>}h8nСΝÇl6^Aͮ]cccO>mmZ O_14)JJ>.]Z:eJ46; >> eڴiW\#LhMI-c2)VN}XDY[1HIZZիGb4__T@,kj,Wư>1#bAQ@LX:bEٴmmնg2d>qĚu4ccc{TSW7 ʣ:"";V*T|^5Vf)TGV :*h;;Rb+">rIB8'9#g>gǺn~ gٳgR?=zZBBB Cñ{D"ё#G#FGGoذ!**²>w߽͟ۦ3^'Θ2-_o޼:K.}g!:u"8o ۰j4)e_`]= C]bX5E譐KꕋkՁ)eK,[Yۯtt iŽ$@>[v%KZ*77W_"[ 0Ѯ1ڶ'%h.tKc+ت*m;TUP 5?ȟK$H۾Bs?CO&؆Qk9kxZS'Nwސ///ar[Ӓ7ov133s.r GJ(;L(Ⱥk !dt-gf(wߴof(݁W}s={m5%.\=v+W4|8##&uJ\V;w>#{PI@M}daaD"+^; \P'e2"!P:Imf/xX)Eh:Q4C%"&b{7{QɈb^Gﵵw^vEaGߏl6nwy뭷v޽bŊDa[GOJ(;kߟ$$v.]N "”3L |||b{z̪]pws1zvT*>}?G6I;`.4 +FW4bmצiښyyi7|3((fв5?ի9i[WhSD"Qǎ7oܥK֞KzڰЂZ{ #]@PxxmTTK.|m*x4,{Eړj}׶m}6@*k۷ooR A@8ѱ .k~*G+jlKף\t5pmlԩ=\hh sv=lذ~9GٵkΝ;7]nܹ#{%K_|l6uk֬6mpMaÆS- \r}#3s^{ѣeee+Vo~i ,pBNNΚ5k~{B?!$33>__eee8:|𨨨sFqoh{\\g t;w/oܸQYYosά/~g_u>[\`8qz1{PwyrY9Qt팍qØgTʍr<{m_ߓ9<<<''ǹZh]ׯ_1bL&իηgff~#F?|VVG}$ZjUtttǞ,{BǏP(/_w߹u!!!ݺu !^Z{Xoƍ/_%z׿{צvpA.{VNX#V'638 8(?HU^ G5 j>m۶Y Povll%Koݺ%̜9SJR7gO/%%% >||{ee… {ҩSݻWVVuSqqq׿! 9rd!w>|8ZiiQ n'aޫGd5P{ԿQç\BR*VT.[.f?( lٲk׾ """/o>h \ٳG|5kW./;;;##Cy//N:YF ,lݺ5$$|׳vq…N 7ϹvZqq {ァT*nܸqUMݻl6oڴ)!!i&pܹA53gpGgޒhhŽbbb_~띵q5ڏgʜ{###5ݵk^{ٳgBzYRR]wȑ[*CuSaaa6m:|ll,!$66>͍{C _;o{ؿv^6l[%jNq)'NTT{5l6aRJ^y啴*e;,tM2555&($$oOIIIOO?~f9xԩS=u=>3NgX3of+Wj4GΚ5|׃N2[koz#&8cʴ~xޫ[j=֧~pB__7xpI&[K.峕}N:k׎_C֯_T*Urrŋ[p>-+"$%%%%%5lwV```o?a !^@PxVi+xqf0h>@PRlOU';t߾m7+!Ք)q}vC}ƍFDp55֣G 7:FB}{^$qVzQo% qt;koCfy^\m)ǎխ\\"E`gZ~6z\RN5KlkeJKE2rxܹ !~} 99Q+C9f (O?<)ob{U_!B_MH:e޷z0g @K-T[ǔtzzӳ ̅ nw-[={ ]tۙ30yy^viΖcpTK9aq#ðWVoؠ52_Jݤ$9_|7vO+pi.Ka0cF2EѻߟDΚo鐕Ŕ.ŋuu_p ;^yE=th-V&h&VTTo&vH* +ː[瞓##}~\XX~=u}g~e_X2h]*vLq׮knG^xAjw߿+Ms$AAxet~h1cbbdaaӧMb^UC:)IN$ih:)^+"\NLgdx*  bX2t2n߮IIDTz_W+2RTtg4D]Ot`}۸k#uĀ"1LGs'.wtRi;&|v483d]9޴kU9w.7:j\Xh.,lc%~6-.ٳg<[[T ^*z @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@Px @ +W*T ^@PxⱋW88k[sBi.[:3}q*iG{k=/ 7Pso[*iԡsvj]"᨝BWirFNhi.XC)eD2rR=W[j?_嬜=5(fFv&([g[&*)88a ]rjkl=s!v; [ r mv8a3*z).Ys!JlL)# Jc8*[Z9Jw.RzeByX#VMQ)z+$q6@$% ӘRFyR]5_[VVoۉs@fQ){AM Ty<}s },eX}){eV+"HP'sQv&E#  +^l=QRR HZktrzeԉꀜ""L9p62""yƴBϹ7̙vZ諠y= ]jA +|b} 3§˷RZ^^>m4QFVݻ7on׮q;sEz~ ڪ>Q@mr w&ީLtjጚ~U+M&mjcTw&Ѧi}||B)Ӊ'.\p„ *j)))νnjj_׮]ܹi&koJ8qEƍBWmv8a3*z)3lgm9b+1$P·+'([#Vثlj(%%jdz\parrS9qDrrsQg6u:| ]'N`ԩSgϮ(xFJkĪ)*Eo_Ұ86%e22pSHi}Pȑ#_~E8Xlو#yDRoÇGEFFR!@ʀg?J߶;IEA_5gp^ԧ/`]| //^|2˲rj6uXBd/ȴZa*7g$S>9L]S`Yb879sO>EEE[3j(̿|W~{Y'+X>(1!*bX$)Dav&E#  +^?JJJ'deeS*FEEeee ]O^hȑ#!QQQ2gϞYYY|ioxFJeԉꀜ""L9p62""yƴ3gNnnڵk*/g_wM0!++ƍ"ĉӧOo;w~wʕ+W= CJ߶kӴέ+Bl%СCo޼y-[/_9rΥz(xqXxl!X=Oh_+L[צI@q3ʀgz n8 <+WpϠ8n8^@(xp` @@q3((xW*T ^@Px5<ژ{hA(P!%NVbh CI;Vss'N/?=29NՒ%)Sdl%%lVK鐝_p%M3wnKE߾q\MQƍQcpAS+"viSA}{^$qVz8?.撒0 .(pNUWLs,4T-t;6VW- ӦUoU*ߩS5fi-#X+&yOl!7)-ds/wfsa>3S㣚0◚jٷϐ0ž=c ^չ)(nj^tLyyuBy~yd];B:>;pްegOrD$Gy d)*Fimgp6d2)z7Ç9աvl9vQ]M^o).JM}?ҐLxɔ ǜb9xg0_ʟ3ׅ^a0k3eݻ3˟{37.F2Eѻߟ}[bYP7R%xn6mіxvt%kjABWLiꗒBJM;b :Lz-ٳ{8Fydd4ubC'W3Y׮KYν̥K^]2k " e />~& !f^~y^c^nٳxwoQ'&JCCD" W'%_wu-[vN5~~^+"\Eh C%D,k4C.5s&?.f endstream endobj 1098 0 obj << /Length 2867 /Filter /FlateDecode >> stream x[mo6_8-EQK ^ҠE9CU+zc7áv%_/0p8<za/ޞ|sqrp! =[\l݀X\/ɮ+yTV/WҊ$Ϟ\*p0{7'p^pb2G{ 0XVLOyb1m]W^/pxPp  a(~6gZl`CF{1FOf; =ǕZ!hsG]Ȑ4U^"+ǰ߉C,r ˆ# ? f 0\X@@o4vYBg$U*oTltQoQ3fquk:20Ǖ,pv& S}ul7ÖB"=㿬VK=ۜ ;0$^=It42my6QHoQfꋨnҼV*gfT$9sCլU}W[;6Z.U9mMld(,i^`iεLc\:}Gg++ Y! D*\_|%QbL!1 F/`w>hw(㞇tA0 > |Aٗj|u$6uSTø7Wu  t=p2^87 Q⏄ s(8,!~C^!H3Mΰcɒ1ހ_U8KRӳtУluT<3NkFm!MD䪩[wmRpj3)f~X͌,@8Kz#hxS OH+&-J1:/M@" Z!=es]~\}4FReAt q\~@:WOoDXa&I:I u1E"bn[[dQSN=2+WM8DejQM% ɞŸIC1nŔFU\leVUUO XuT:$$ Ծ$ć<POp`r\gqV{J*ePGw#9ᓣ<,6}DcMsxXY0ϋ(:6Ǥ0 xVR"<9ޓ={Ar4(!\;Cv7M;Gs$+(.'.&t#4 2ߵAԐ_&?uDg^xQ}Ibh"9h,@##6rS$@vYvI`#]{E,ʓ>yY?N=]j 9ɳ{䢽: (ԉ:c 'c=N\b/c MԵzycuyE=)$f̈ﻡQ("i=Е-Zb8E{ *6s 82,Ѯ3&Ѭ'&gW_%Y6ku_l[GA}$LB!siI#]'&5mk: Sr݌zWVN`$[$kvĦH;`5j#/-\+'iLĈpG@/z~Gn:VEg`ȃsE=ch 8 vMOIR]/iv(\_H"vֆ KGCA@\eu*"dG*r/tn EUg7律ݒ61e]dw&< 1Y`FzaZ튺7Z$ez;0I}3UۡO #qU:Ϝ)g=?LZEl ExJuZMi.s0wn~9">.]Q_!шvez5֑ ͕לo"| yrSNX5"jaG,w6>G<'(َnN7ڀSZVE}ITvA&:Pd*AJ{'Z#gރ???. n1ۨXhf๡l?AN3٧]Jf\7eTa> stream xZmo6_`MR|Ӗ떶X]o]h%GdQ[NׄL - X=wSDЫޏGːGIt4ATטG!:Jл@~5OS_S׃@:-L\bދYzQXi{#Λ".5\3tG.gV`a(H @Bß kO7;eNd8 ЋPOO0A>nae Ev`DT󬮚u$ 3mIJakUFQ)&M}jR3`1O-t5Z(ilGFP*đ҈Ш4hnb a - 3 IG[,v+SUM7\ۈޘi\0`V,F(r֒''xSjij\Sq:tW;6f1Zx ѠQB,RsRˎ_zEUwsKc%u=WOT@f;'7`b<˙vszmsK^:7A0h,x+pnmkKve/JSU*|6¾jgW6𪯮7pRk,O}kU>0)Q't4X& 1WYna~\bj{r𦏘N8ͼ %Pc<.^ux #Œv+zl+׼aykVaaSumƍͦ 5eMk!yJ!Z E Am!GqXulɰzo#chHp8oTd{Msc:%TGքMUdtð`lm endstream endobj 1095 0 obj << /Type /XObject /Subtype /Image /Width 431 /Height 186 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 10725 /Filter/FlateDecode /DecodeParms<> >> stream xyXWQV ֪"JqAAQѠVXuyKja]@Q ګ"O[" ՠA6EDT*`%qfL%kr2s9wfLzvtBH+`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!DhǏۢ!Ա40`V/Bu,RF!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!!D`4D!,SUU@գˀBB!B B!B B!B B!B B!B B!B B!B B!B B!B BeD222jr Pzh Bu,RF!V97$zkeJ=`vv)2B +ehBFChŻ(>|8`ޭ܂EPiRuhHU_|*kU3k-UYy99 edd4z޾-Ȱ$4ʹTWiiiׯS H3R,!!aڵGSetsCY!!!ޑyٖГ'O#G۹ٵxn8pƪ_Ç>b++%K;wv;bҤI+<{lʕ]޼ysС3رMQD<~xݺucƌ0aΝ;!onee_xyy~Jm5|ߟ嚛s8.뛞N􌌌ӧOt{5.իϝ;wsٿ]]oZTnORZ"v9a„1cƬYFPz㹡CCCg͚գG222DsssMMX,uuu+V_v ^~~***;f罹fٲe=}'^zO>6VK}7 H~rU3{zz^tL9sF__Y5%&&R30eQe7:44|ż?d;\N>,::zĈT:H_ʕ+_Nttte~!C޼y3zh?tPT:jԨ={ԩSKJJ -- 0)׽5y䲲2HHH G™3g*]Tc}}9hT_~eNN|駣G&fff2ѳgO'''>}E[KД֖@Qٳ3f/MmX,i <[ˢQ?I>Jrrr\cS.I.?޽{S)=X,[2AR^;zF)kbbB&H)EW^F'PLMK-EOtEQd[yUwhbx<ǣν)IxxxiӚs~*:$G'xoқsU3bRwmmǏE"QPPPssÇ'755%''㏯^rʠAm--M}ܰY\\L}El7Dٳ۷oRiSSF;_EqQҥR)000?Ǽy֭[RD":v옓D```uuuUUծ]4̈́I5466ZXXXZZĚ*r YYYW3eʔ|<~{ޢtmKp\2{ݻw}}f^2*ׯ__v-XYYQs ԁ!jOVJHHkhhD~~~$}ѢEZ.v(//$Uu8'iAoHFBCDDD@ :t(O˖I"26@JI9CRl Dc+++]]]++۷_~ի-Ρ!uv*;W|Aj򥟟+eH}9NF ̜97UD2),,trr"4-REEE|>ё $666Fnymyz׿bbyyyrr@ FFUs#Bi1'MD .8VT翣j΃[N$?~<,,Llnnn˖-bx֭rogFTzѢEWFFƩS d_=ʤ.\sΌC]~.\wX'''U#gƘ]۫W///OJBOO^zQȊJӦMׯ}4-mT57(mU1?x9t6&L5j>ڊUH$~ZZZ:thDJCCC n:{l6p|}}kkke!3f|'3gdR~E!!!\.f: RQQ5yd6mkkQ^^.vڢƴМ9sNU:,^x͚5ΝnY>QҘ5k5'۲ˎdeMRHI4ٸ61-w lPhhhX]%%%ݻbˉNVvU͍Ub|||ҚvRgCEC2 Jg;|%K =kMш99r#ᥪJvq2$+ş9R)d2?smQcZ߿>((p8={y_n+W477Ϙ1C.]eFֱ455uqq  <9hQrK$ϖF򱦦J@$}gH -mT57uqƬgϞy{{99% "vРA7o&j!J.\8{۷;J#""Ba~~~}}.]J @ϟ߿?11lÆ y46mn;uͷ։ ӳ' $qȑ";0mki2B;fROտ_QW5!l^ˋP5-/B"FC" sC"0":aͷ?!!!ѐ~׭MCEA]R~~~LLLLLLzzzss]B?7oަMhϚ\WWgjja6M_TooV$6 '" >kٞ\]]͛'Ν۷o׮]:uԤM6eddWiHFm>3xRFHֵk^nLxbrr+\. mʕɗ.]R:PPD"ٱcۯ_NT=V6{ʕ cbb\.Yr%LVii?/8889rDv|CWZZb r5BՂrW|eA 4jjj̙39θq\]]^a=|B'''j\$666F,;˗/+>>>88833s޽NNN%%%j~>>J!$]۷o?xf-4nnn˖-bx֭gϞuW\\p¸;wfdd:tHv 0Ө}Ri\盘N2E[SSӫWN8b 43MEYSL111111RSji Fi&,~hlݺul66cƌO>d̙CBB***&Ofmmm=<<˙?eǜׯ j|*Ls S-Ʀ_~G|!!!\.f:t޽o|4j_rai!9F PRixx-,,NFGz\&d4Z:96l<}&ЈYfQ)d۷oΦ4CDJ䙔6͍b555(-X+++U#[XXŋ׬Ys9jk#B?FRoIIɽ{zi!+: ;'krrrrrr@٩/_dvf&ý7440lȑ#F =!ﯸl"9*R)]QQ6GiRHtE666...[lZ ^daBBbo>R",REm1o+W477Ϙ1(UUU| E/Lvtt4޽{o޼1u/O8~6jxAY@cΜ9 bdsNz2mgg9k ]Ԍ3j* ӧgddPXX[B<Ǥ}ɉY&;.kׂJJJ&M$lmmoo޼@<]du-w~Gͤ +G6^}}}DDP(ϯ<$$$))Ύ㙘ZD8Wn^7p@GGG777ٳ9 ӳ&;wܰ0.Ѱ) H-MG)**o5bͷNsP&w2B˨kѐyY%@;.PULђX%@;~ChBDwy!!B`4D .!0"!v4{}Fv2&OU QPg4^X,2E,=z4%%E"3fϏIOOonnfXjjwtt4Eߚ!!!uuuNNN6l` ϟ?ṣG97|_oΜ9C^]oiiٕ _~~>>P qe˖}ϟ%CDZ__/g>[3++kѢE'矮TS۷?ÇΝӧ߿?ٳg!!!0A}}}{STZXX٫W/___ &$$thH|\իW'Nb DF&)S|TJ)4Zp„ FߴiȌF 4BBB***&Ofmmm=<<˃eg+..^rT*=q(]kQC>&ݠ O._؄j~:!!\)D`ccӯ_?]]]KKKr -((غuٳl6%RYYrlСCZԶ&Ν;"d3gɓ'>|HF9sfvvɓ'T]]]!9P1dtT* ?ǎ /^˄ZL0?hAذaS&@#11rc.))Iv+W՝8ql*))w^޽\l:9Yյھ}{SSkbccTO!/^f͹s窫I:Tf(i;rCZGn[tsscX>>>iiiMMMiii$MJJBavvP(444tqq᧟~*++.Ѱf@ ׮]!m(-KkhAjf]]]hhh` 4АJ!犊 يoTGrJss3:HgbGRFFFk~Ç/Y0!!Ύ^r2r#F󚪪*x "l5mll"##MMM]\\,--l&3q8gϞ7o %JbK4$L#5A;۶m355eXnP-޿ko7ٱSx;={\~͛7[Pv2͕/S>b1IRRR._<|D rrrrssCByyyH ֜4iRTTTffN>MlllȷO<#FPB*۷qqq\.DCry=Q/͛7 Sԭ@9dbܸq eo 2ƹl<6,,G ߿'{OIP(>}̪޽tTQrM=5tttVZE>2\*xݻI_pwׯgX{-)))**ud<|p}}}JJ yIylx߾},k o 7`@H(y<ܜSNHN g͚էO)Sv$yxxP/`III.** 6olnn^ZZ*JʄB644+aҚ7njjjz왷w\\ȑ#o+''G*>zãG?3!00ԩS?IJ7zڵkAAA%%%&MԷUUU7oupp .Ku떇ݻw?#7AՂr%T|&{yHHHRRRee311Q7oxַo_zxܹaaaJX߼ysK.O}Çϟ۷z endstream endobj 1107 0 obj << /Length 2989 /Filter /FlateDecode >> stream x[o۸=_&gHJ*uCz)С=-ǺȒ+Is#)JFΚbD% /gW?I,}9XOB8\&ki(z_\Mg~yߕ6-LxI7MIF"B\x3S CspܞNVㄒ &QCTR{I6QnaX]^$0My&gRo٧eQ50F?4wԙbʏ{즓a0i9+DAi RCW I|n_93E7zD_tPV?4$\^N6 ҧ^z.`F]I =,j_+}5ӐR_{S+LӔ*@ 1#,d <HPG!~hrZZJʢ J?Ӻq1CCߗ3-C_"CȆiykY"  -7IlDϙD%q%=o6rL+PV(=rγ"}`@јDAԂ`>3&}¸ѹ#>YgffMMOx֛$ߏ@p2yHhDZr";Z )ܒ!ckyrhMFK리q3˒,j̻9sDGL:3\wEV7iu Y_7e5vj DŌʗ㚙( fɖ!*;Hk0%I'"|~`zNneY]0T{<$_ؾ\&9v'|Ux@zxz 8k!~ufԓ#]d>]< TaM_E$b~?}u_dNvcE H9٧;9 CasGc !ێAoRH]jɷ-~:.ͦtgGn9 oɻOzG 4YvMRU@HZ,=k Ҹև𬘦o ^1ͺbmH½UK(MS#S_$+2h[੝ai Z\33 DtNXP4Y2 hMIj&_KWǾ!56Os/ w|qlG-=%5୶+s|cON9 @hV#vra}57?f)PΐX^ yл]X%X'T5&Fmfݼ7yeW-Cy$ܷ No:(Z?,R rހu*Dx$1 1 ᪹,Dh&lK?Q!*|@`(5QrY;V,4宱ѽ{LJ%NλЬWl0,[URCܾxݻgFV! qIHh-,/RV6Vt%&}2θ:I@5.c(CG2$)&#OBD!y_{ ~D S6׏ \+#1[sW`u endstream endobj 1112 0 obj << /Length 2783 /Filter /FlateDecode >> stream x[{o_5*ЭC[8e;QWV!x+1Zwfw#)ʑxPAyfΕCߞ $_:K}w9_9fs_:/̞qVY5_YT%yvdTRFBϿ;x~~iD>x:+hΡDգ֎:oy@-TWy8{]ISpEH p|F}qUIV0)iS9I^%Әm@ym eCJ]_r B9ۤZIA`̃?[άL<'^,xq~UYݟ4 `l,-Ch 2Bc0 `0cF3Ƴ`d.ǵsG=9[ӳI##DLh 6LC~iT4"5$'zx 3y(gGf|uw0GY8>r1=xGf :"v{SN@եLWهMT[|vCHc*;jYWD|bUncu x[ШT3ow>CM`|F4u+FrrxʣU4\](d!qNl1|U}xh&l2 bIv`eMͅ} xA+pfw`~ &Xbeƣhw,R)[pΈ+M.M^g#o-g{+p}DV˟<ڀǁ=Q3B@WJ{ӂ1g3xl/:]ڥsT6Jϖ"4#ڦ|@oH#]YKkx'#xT;&" _} {I0|nU1P> (L[JZf!⡪Ko) },a@C!RJ v}LLKUGc7' 6ٝ[?`!K54Z~nhKvC|DjALZH>ks pV'>pB4 0B>X\m5SiDe p sh#D*,ĮYzr0uHF[i9{P•xHP>.BBDSR[آ}K'9.['hIu3FūDTuUms96&>! ĽS =M`MGZ}QZ0jr_AAz$VPuNӱ7e>9<4'Nrj?q{sPIj1M7>I[م+(6 Uك- MUA.8o0U?(ahۂNzά6i}%du e a+l`DJ$֦C 1AKQq[ L1,=vgO'Iddj.Pb.f Vw&Hm  e߱ⶡQ`88-| ad3H݁8-*{i>,mGBM PT ($.Pe;X+W%:krs)V-eMB҈r!1 I"}]wm{ lqav@FX:<}ZFn:fد#~&hcfn7A;w&/q]ۥ^b !'2m1]hxߊ![2L__TCmduiRue80T{-1[h8o7z gnd毻/}4] v/UC8~̎΍D< hJ5^N:D|9,oNo۳2/cj˅)&`$'*7:;V;^}zR64L{UWbOpWO*[}.BǯIs*EfU؋S }ju0)I1_6 u5UحTwSg >כ$Ԓ5k,tuRwK5lUoך fJ*Z?QvT]5?9{~4Y z?X-xd_dx.N^lL<Ť~|Ҳlk t_F4m2$}g؛~=٫o9_xxÆdU֝Wl-=rY9NUs6WrW{ȼ(BhH\&Mբ4W#7UyN*°&;mo19_©kp9aᄍ/<7_+n]M! endstream endobj 1116 0 obj << /Length 3041 /Filter /FlateDecode >> stream x[ " 9ǥn4H|v[8|%R!)Aξu֑a"ggggfv7dQ/BO} yKbIMLg4'v-r:LԴ˕ i"<⇓' {Ĉ(X{KhÈEwZ{s=? jb[3OA-梷ُ&#LyP/P K ܕSkDć?\h->sDEd BR۫M(EZ@4e <@0xZKM|xoW\P9fğ_ 3TL6yv_& xpfuYn=x;MF܌c\BAarVˢh|+G[˧(cEb۶R39P Vafudpm!6M`V_'ZrvO_CF ,5>Fa$:8>?D"KXhn[`_"O 6ea3j3Dp PÇwk%Pj0 0x'ff50sM-<0 QϔD۳~n8JLS'˩~ L#B,N!]6Y-[7d84m3R`jp)!2HB#|qƥ meW}vzlAv&JHvu{y}?>ԓΜFpHOzԗޣTz(Dܐc{Swg¡@W]Ux}96"$ؗ2^H9iI#.T\^%r}Y㌳ϟ&Ǥ<۔O^>ZZ@J=eE9Ui\D7ڒ\_Dž5L]4Yspp [ϩh#jDvS5;: )˥t0D4⍜R>m~LtvV[Uw5&{ &;UrZU穳O qR01SGwPnx~Ȱ8K0J TEcZoߨ"pո\( pKӾs/ 8:$: 6NW=8KJHYVRCv[8,:g>4\=TbEr|Yvwq:w[ryy^ݞb}*0]Uݖڃj0yHZ::N}~b-)^ FQح ^;t--aMaơw}G@}B Fj;c3 kCݾq,cp Z R,>ēPLێ^QMn&ٶL7Og}W_}ӘS9*3 pu*,!‰6y4y;NYL rZԣ+{ھSP=' endstream endobj 1120 0 obj << /Length 2397 /Filter /FlateDecode >> stream x[mo6_ KC]o/^(6 $W&/,XI p{wޞ}qvQ/B[zL xxļ_ᗻn6A/wʪ\q+[NmT\*BPg.8#0P QC,ӳ_ 0bQM2_gM[q»@SA-iG,@躞 EAynaC:-''~0k9)Kޑ戊 uة2jISip̒i:typkCݔճi—US/?>,%E57_?7"Ă"!U*(F iڮ\EQDIM+Mn0@5.7]]b{h =ZS&KQUQ–<_'ϗXc0 B9D$HbWeU`SֻlʍRT]]\\勋?۸,y מoz!' (:*VM(Q$*x$Ks\m^sآ*[+:Ffi7LjD82#Vfm1Q8zIg/C o/'x:=C(${?}HKBp+a8 s8B44zg]ǏF}D 8n-gDk$nTa_u%e fe vI/ Ey(G0V _F)&6Lɽa]v9xEje;ę+$Gؕn`7}RlU [WIsۻ'[QzVh߼L#%N⳹Z sx%#APdj: "C),pp0jױ[ئ4~0f/LNAIƪ&:R*WurphؠQJQJA}f~1K_#,5K(#_acm,&81ωM$LÕbBj;xIiKJ XĎnd ;H;bGB>10Wd#Gv u¦HbpXiUA'"c(J '!~C^kI|Q}s:X%r$Eb2@4w0S.U8A^## *-f\nCWF~~)}X F$h2eoF~n`Q ZO. a?e/'<0AZyvX߀3XR렂\ QiL__0X&^chjs1;h8_]}gD]'YuZ^hhZgC^nZǜ˱I$EwpGjEv1^^fw$cه}sgu'p94ABS{E"<{KP{\X#N0/[gX< Ύ[My;m$d>{!ZGrd[}u3eeZX:Ddo]? &TpMWx ~|}qk xؔfŦ a.65hsG 4!Olb"0LH\ "'a.e`bPq _*mn^Q5 Kb"^i\>Ÿﶶ% P@ DtvqqJ∬B6dUtcwyEu)EЃ!YHY.1`XE\Un=nvձC0'˶  \$'OHBs%G5G>ܤ9SjFl,Yֲ] \/wO˥ռ4%Qۯe眂1Ш1=&|KG#\  Ao,󁐛9xM߳Z4iA这 _&[ BTRޚzgj endstream endobj 1125 0 obj << /Length 3037 /Filter /FlateDecode >> stream x[mo6_咢4 r%M|@NpЮhHί!ԛw}6€%Qh8>PK':臓KO~899D$ b$|rMNi$fSOg~{,t&TUB FbBO^89tUtPt@"MtA %<'z1?qD0Ў?w&5ܸf׀%G87¨'>.=~{Nf~> /`ZI<ɮt/>O}0Ie$ngSWpA5')O6 wmneZbF2rʅ鼘r ̬ЎL&T\@W'g)>DV[1kA$MMyio~$_,w3hDՎbֶ`/]umв 4ñѡ֎P`KWLVueӀ[P*wօ(5-ω0<20!Hb"Dn"N(DOj99) P_@`\ Z\P0BM>姍l "}v wЪim"h$7:Fu Q.Hx ϏSo s;*(+gy޶?$<1aѰ&13?dDx8+.O\L9WJdsz:sRl?~zq{?Z'^=K|RD/εG_ ,F5k(q[azI~zޅpKr:" F$ "hE"т2v52hWVcX1Daǰfk̫MSk\8uZHڕPǤ79ljh(:p.t"?{'je!%^d2U2k[u+<4PyИ ‚30 70Xg|0 _~Rnl0@0>zO`TWV {U|Ǿ]]YJ 2EEbO pXQ9Z6Uؾ慔%奪p,e]rފm j7.iL 8~V ՌRyaGs*[Ia:996@9pIK=(z|g Λ\J<͕=6x\Ud2'-'d1h?3Mm%[g5 o_klߴ|3Ɓ31Sȥ~= l229xÖYcK~lXOcKεDg:AovݼE\E$":mk- YwKBV`$-|Fo Zԅ(@N&KaKBB#Lǣ{TLNA/ ['JqAr6kph[[7hM`cb 6q+45'N3ZVɦIi 9mrl>?e\z$DVO  &0` qC_4.q8C)hl;*cn`uG;ьk=m h`-$v5c El'Xg g ؔq̓xzЉ&V;4b/jZS dU8ʮYo&VsWaM^*d; ڄSsӐ#-B%)c;~O)JS*8^TW·=Jp(6ʸ:{[}VKu&eH)afQ`>Z|6zo0 CV1rOz!EOY6*"(8zŒi*xt6[k(jD殲fK0 XW TDwӄЯ^jA2d"꾹oCڻ]ֲ*UTpDv\AЛ=4y}ј :oݘ/[7z hIFY4jO]?72} k,yOfRz?>W[)5ų^pպ(mjE1L{xWyBOOOkM83VSr}و\ Ƀ$1 8!Gm潞 mο![> stream x[ms۸_ɗJAҗK\Rt\O`TH*>{w)9(kn1z 8$!&3MڣDDweyRpM{ŅgG/;f<, C/`(YtgU-g5/I(%@nc]Plc#5{DY ]$=.w^ghP c IHԦ9(bƌPEJǺpӫsHޢw`-VXK00,A..t' 8yUw` ps'[3FKQ# a˪qt! _u›!]Ү% _ € C1A$o"CpK{0C1k.hS#+Qo;Ęxٲ Nn+o{= 5\[BhYhw5Jӗ>D+ǧbH3ǼLWK7v(aC`N@ctbY;+6F^p3^jYs+MfIUb\ʶs{ :F 1n-dVweJ$WMlCLϪ'\erdTcVP%T̳ZV5QӱB.$M45J IY:}J]ksݙ/ G }dRǮd<WG2^Tnz6Mjۢ-4(|qGǸHq< B^{M1JX2 5ȣu{Բ>cd r]\,y\׽y+n`FqA$DzqQJ7CF'y)13QW2jW sp9~vi(D ^əO|׿&{eމxZn_7}6NeՎ,\*2 ؽIHo8̮A@@8.,ނ,+;£ TA&v .([ k/dh}`j pla"C 2arnk` ۮMM@Æ cv@LVXZ:x?T L'Li(6DJ;VB^Ex Iьn! s "_n=aV m^V._Z%=l 5*z*B 8 L}`b%Rx4q 4Qj8u),Ue3Yt  -C)!Er>pe璆`6kBI6AgNx3έZck(9/*e$k2mn, %+-Z'm:_8~f(qQv|`2ں-g`*bjBLM$RMŬ,UGH4ϴUbUO*{ݦLwm[5 $u%柆w\]Cf8L5LMRtE%uSB(0!@5(?4,Me9u*%(7 2vFX/ +óggrOEJFEG@:b3{1Ldux(e\ fI 5Cnbe˄A& y \zL]lk&r_-4Ua1p&6c!hԋ 0F2C~g G۵#JxK3) h ]Az4D1S^(TnX@\b UlS-qvQڊl0tEy'[t29/3/ekpo6Ɋ/k9ջ.'E26W$MoU$0ₒ(\L0\< m<APr2aַ܏1T>ƙÐa(3Q^d($LlN.> e ;X3EGY egU'ıؓ~q?ۋ'/:\Yŵ%+ׁo\#6~h}iH+溚#7vj80 v͡Atg98]W0C=%8Xh)ylhRuɞQFT JHf, 碿t|<]no> stream xYm5_a걯  V(N *9iN2:TR3/XCQe*u!*'bt+$BV*HVIgamm%|ǔ1M:Ws1&g<9K2< 1~@*02Fs(z R'E]=9 X ((`xĐR4"h$@KIh>b (3*0pXVd", GSy#Bd%%qS(pX tY(0)`d:p 9?c{JS xY3!˜c ?ՔdNtp&Q+DqeXye<;*H,{pB2 3,f&W)lfusPAr`V,T q1 ΂c;c"8hJC›>) &gθ1 S*cD,4TC# A#ZQ Q7Ϣ-0DJivv6kv^j` ^!_7 h` 嗳[z_?U^]%Em;k`vٯ%`EVv/AX\ L4_m raW#lM[nuٮhy;+34ulΛ^'jp̕w\y;+O\ #} 0Z0i{3ؾ?HgS$>p+yPue4:3}20x'];L4.L4JJ@O2Z pASUO;Q3QluDU,Eo}ݬ=~8붼MۻsN3 @  fX}5ك~LK$NggdGvau2aOLd# 1.݉dKgur\4jwՇ=@>?z~;_\ uC4K6~ߎUz(&wR=  +Ft5@ 9> #9[ȥBTn'9K4TpS5|+74G|gm! ג:מIwSpEf%*r5<1pHV;}>/q2WTye?˙[ F'=ާz?%u#>J轃xT IIu`>{p ke*yzqӮj*.]4[G4{?"E endstream endobj 1135 0 obj << /Length 2348 /Filter /FlateDecode >> stream x[mo6_!Co*JNO-pHB+QvfDJ>$g!)]{'goB.<=-"C]\YUU7In 5LZ䧶m%QFB^xǬhI4jsWPGﮩ5ޝtB0 3= F 6U*mAC4̒1y;b(+f.os 3_ȐnJA >)li{; YU]GVX \Mfk@&M\y:^)|;u*fq;wYc(JgbSdLSP%k |o 9lֲkѾVJܘy<{s|~H(c 4၏R 2Ļ9J(GGD?zeuI_]W2N*/ |K0$̖ި孛\LkDJ\Ns<$?X$ls^%`ֳO ,G]f7V|wm_g]t$gboF={? 1Ehag#lp s17S]K0"2egFF4$f]lhIYA:K)cz!'!BؙѾC`>&HHq6nX 0&3|&'8֐8* e8N5~_'fY)DeO2D|Ysq2ŧRi&D y$1x0͂Y;S$7-Ѭ)2 f(uHԾ 0×!28b1b!>x.J6ala8Dj~khZqQcO?dO6oLfI+S0Cv?-oG!܈kyRlAxwLdHZ1F~ pM| Jb*6m.ڈ}t| i[%v-A$i! Ӈ,"NYhz@ d>;sҠs6eȧ؄_fQ8H]Y\8cȜQ`$-1y8C8T&*8hI> >}A("|=A)2ZdOBw4;l|Z!ܢ;>@w,no.]Ad/u !g Vnz5hŬ̺gNVw 6](c(.*>Z큒zey«IW/AJ)DkٟӿO?7 yVTUՌO'DMuRsN>"0Y?iSa2@:?6ng "ڀ)٤ Vˣ2hd)@Ȍu{ѧyݝqh]v_"gφ3AI1_-qMPn|2_2r\xŃs3|9= ?. y]vк6 endstream endobj 1141 0 obj << /Length 2781 /Filter /FlateDecode >> stream x[oF͇+Dk{Kr=k^b8A@kD$>HĤ[8rvvfv73t\Qٟ/ο$Q,.*$b $S4ȂwB׋%*k6hK&M^OI F Evqi$,n޽ _ nMu?g5Ϫ]{bwA@XF #$t)QZM^tL(ʍVzAx)/wI͈װ!80`]>(*R"$`Q$q@bJWCR$PD@`X[#oLiGv r2y)Zڄ" m L{ 㰼X$$'׻͓ѻh"b;v_ޛ^hJ(‹Mv[!c $fp DQ'EXY4 lYD"fwGP;4 ,R1>{ ư%cDbI U^+jPÛB&j8e^)b&nrGS{tUoDaȂf@:d%r'S0^]J4@w18h`ΠVDĆ2!9{wSiRe.Mh*sr&o Z˙W:^Zwޣ_HJ܌֝q_V +obJxw5t^7 $! ;D ݉3w' Ud_.GԻ !Gpt}7 %jnXvjK?e 1Zuݮ͌3~^}@E%#U*v"6.- o/9 iKe_$L^ wݟxF&n / Ƈ7Ht&dwҋ2ӳNA@ gz.ou6[TB&p@P @b0!ܹ`4xwŸOm_3?$V HR,[bZ5ݹ畭iZ82p<A ~IW}R6s7~NA3[NVy*(4b,%< 9y M{39 '̐rV3\ǘ)2mRآF_TҲp1-i{aÈhk^"bvnln)Ww1޷p S;w54a#&!Vx2!%W! ^G>h:+6"E!I@cޖa}1hWݘ # \]l)-ެtFZ&TablivF6vKI,+,AJR p{/dXYt?m%}RRW~Kkl H0<`0K.*J fzx&^A Uw<^erVN ɹ bFF 3Q!?P|lNY~iӣUlg FTuѮZOS׹hNc]ec'/̦ͪUDs xAhmG3aMYe/ 3&?LsJ|s6K0:]eLEk)GFnqϧgB!I/N} bal4.m?jv}Ն);߼~vaXBM)tBwO|vH;!/+գ}敱ͺpxT-YԽ/pgұ 8Do;pM} ɴ#ۣ{͏}JH}M"t|?CYg5,Xkmc~'_/??$r/ÿ3 V-7F'%EjKjRA ~$ivd=N<3?q6 endstream endobj 1146 0 obj << /Length 2622 /Filter /FlateDecode >> stream x[mo8_+PTjEJzwm{Žd}@ب,y&o(N,] Q493 )\9x9ǝ <'CGԙ%l0 BoވLW<{/D*RB'~<{?;P!8Ͽb'h:_Uٿϰan F_Eئ&SJdn%cZ ]SSz"[t>e ;Spދ | ϸ\Ho.G)V3[o֛z_z3.K-u<~'ZuZbFPt< g+H umfRʀY-,)d'.!/?B` 0 ;0";lA }jA)rAMUlPj6J~Bl`BJ3YCXgYjN9HA:%Ei7qXM_ץ(k +\])H"/sZ/"o\ e(jH_ŋ6ήa|eWW.\|+Fk:f a`<jYEApFB8K0wIMvDKO8[saͻF3AJv:: ZiPd'*Z CۺI9k[`4F22b{)Iˇfj`RU0;a$b? MO.^VG[Dc*/ W`Wf`!E<'dk}r(u>׵/礭v2<q-QL|$b.h,c0B/0JF' 忓OU㣰gI2DOH5aiT?2ᾀ&CyF $|?dG##2Tedvś $,PSQ55s \ ukc=<5֥Tk@԰6>xЇ>h7 `| ;*GZ '}0Cݴ|rs{ϟE**sIOT0@y)#Å p\8uG^z^rpOla}!KlGekRINm^ysfd@`vF)0jtgu}i"+"/LNmk8޿hFDsF^좄_'A8ؖQOO0ATN7ON3OrPRR8m2L!`?Ȟ>OX)g 9 @1`1l*ē`''zazyƋz$@uaa~wuOKS A<%'=w݃XX]j|Po(ͩf)qY=X{i.v>;gMsM{x>¹ɵy}x\})٣ TX>@imk)O:υKp{"[IFBFRwwOsg~T!oChU;B;"mfcSL^iR;ޡIm(MACٿ9QhТw~> stream xZmo_A8T,/8)Z4wi-.-J3BR(LNRL<.wa?n.._2(Tz7w q<CōĢ`|Uom/"|}~dIT&Ao/xwA@(H[#e l޳7ׯ@'fIYi9W *@J0H;y(bpED?.y'˒yY{sDEh|uWX*xX#ĂL'JDh|Eg-*OVnWsO}4Q*UR :C1lPՓ7Ȋ%%!JxKBP(%]ɻ:)Š5S*۲䪨))|8)W TRz.i~E/޺D|bXGG-.e'AHQٖ'b:޷c XAcM7MN %̐y0G>ګs%~} C!yO:f#$>C2c%QHT$$:H  ȵO(I։I,/S h afdhunͭƾAYJ{%Nu|jgv~KI[:mNz3,ڏrMp1zNH4:L"9-W&DztGeeQQ-I|&}X+f%M>MakIp.B?#KX` ((6:'nMMS|"9 H!,3 d07`Pݥ&YP''Y1QQo +f-)O x j67+<7Ӊ}Rd&' rb"JCC+ҜB*<0;%L޿YwԊy)5.}?8x0q cwW #(6=n1k`@HDsv:kDF2|HD3щ(+s{2m%4c7q '62P6j8ENbtR1dٸ P,rp>`TAӝ}\@KbOeRF61ozT@vXqd #<A6.ϽC|b|l;mTw TL;IA92a,jf> t3*صE^[2a\òɻC(Ēc r3y*"%ETE9˫Ag=k8dL@2<]HVǜExtB@:)(s]U׻I0GAT Hz߼"P5t7:]vc4N3(sF#&;ø aW 0|tJ> stream x[mo_AuVʗC]~hAQk(E*"3KJ&U_ <޵G\ $x]\y!B b_LBWz2aPە.TIK\:}`$"tg?^}qa=#a|b1dQ<~D6VYc[lf&p28ETYܞTe# hư :I1ޫ,TZ yeZJ\f˩ *O 45-?1Pvwd%XTs5>eQag3>SIǯ!1nJ18_:luEeʟ)uJe^>-L'Ƕ6CʋzK'<[;<"'@VLQt;<{3CQD\'mpeRԋYpW9p΀  I5?O-#as2IS[X*xn 9K9Xxjur5L6uk+nnTN6;;47JC93dI. `_+ABOW366J)5^V,aRt0A_urJTIeO-M ho~ip(ہ9Q;zZY!.[<Bi^;\ A6)WܛA=I!ۊBIwkΖDAe'xEuL9by~4R,c+!>eMu`atF endstream endobj 1161 0 obj << /Length 1796 /Filter /FlateDecode >> stream xYYH~`#dK7Df3Qh'~Y%Q`l`<ǿ*x1^tWWU:K:oGF kgpI@8yLodcv%rEAHy$Vm@C<`$7mU ho\c3gkc1B,Dq3/o( S nhC&T &UuWRۛU64#Exmhq~pVI!Z.ࡑ\iRV pMPz]޾laF?nP8ۇ#"T(#2 Y='4F(ZsXv~?ty8: ": 1G)J<@$W\.v>S.qHR< D Je R0phҰ I1UQl;5.TkQ&#畺aݳ\xloWpg'˲XN!MY )RaQNm H;[V"=B=q8+YxƔG୨Л&J2Sqci9'>9!pK emCBI)DL8?pO8 dCy)4a]B&`RxS O`'v' bsj06K "<TYNs)-52L ?~廏'.u:[Oc^;[zQ}*A?@.;ȏH~*7 Ơ>n `}|#Xb Mڕ%o6e 27Eih:R 8Z!2J:cnj4[w[Tuj-5~׸+v*t lϢ,űcDZj_y[Gz0A?6vM6J $" VJ_N|l?3qM4)I3i hHF`'&R &VVRٖ U98 yj q7E429`tE`(ܘMN"r%! [Ŗ1C H-jQ:=.a^jFm7lg&M&L`wܯh\@hX<U^l!L.+t\,*JҤLjG{]m{ȿ5%``hV [Kd͉Op{A#Ie=b$Au`A^h:Q{qM!Olu]nξgg}(`p@ %N7U6N&"V%"GHCEdP> stream xڍN0E{ŔDqhyJZVJM&HH[Fs={8t\yryWJp_H0ʢr%3S;VHcC+*ZO1^d1><oȭ'DSDVhf 6X: DxU >ߤJ2ՊYm[*: H#+ryENT2W: >_x+?,jNh7T_ endstream endobj 1170 0 obj << /Length 1690 /Filter /FlateDecode >> stream x\[o6~$1풮}%1TGIɒkMa}X&91a(Ns/HhF`0~102F]K IpyyRFӤq-a>MO4Ae"&tGgQ3c0$!8C0܉pGW.-+iz6i+æ@50CqaNkh\<ˬeͺS2HP>Q )ey0b5(kN+y95=СjB(d$X*l"`5m^ Ӝ̓ڹ=XXz-ߝ6xL,- f.}3FZ-y#[Wbd=߹~D4n'Rp;"\>A6M XXEVDf{2Lّ0a\aefXd%v,:~@u$<#V {J[q޷څNy{ 8M=6;"mK|d䨍^sm *&C8=(QGeURnglo)xg=f{m0zkƑ2C$JJn;Nc/ HZSM6ËviqϽ]~U/]]0.wUŻШ+&*&.P?.E2 !E_> stream xr۸_K @\ȼIDF  [9zt*鼨0[Or<p\SH" ?]y6O*Cf;9<\̟FsBeX{&_al|1?Z,ssލ҇VBu+Ab}a??o.}99k1їk11 ٿv~ZMbji^窨՟g`շ䯥aɜ5{ݧ6WzPLKD7ū# ]V4Տ+?<5Ww>78 IbM{f-j qŋK 8&;G.To?F ] ujx(_֤%}eA.VKE,@4~^V+;VޜR1V0Ϋl/Zzp&v)e0z0\yr/-?'.F8{apku'FP-e=}íѻm[shm/1sr pw]O^9ٟ؏y[G^<'K)0`̝M <V:\#wƕXf TL{Lq&\-kG}ԾE=,Ų)Fv;l\}5*,+W^Z gIp[)|U(hb.ms,ĸDztLP 9;{qjYr[ʢ)PdEphτ]SG_ђPhSQDbaOӝkc?A^TgJ>ħuom=ϲUМRaAk=P+O3L0>^O"s-f5ZC+8H QP$I Z;OuQ{> j5ek]Y}k "S#1kI =ҝۍo!  >LՓR@;?P0%hwt3%HTeK|׶a'`(/b5 endstream endobj 1182 0 obj << /Length 2867 /Filter /FlateDecode >> stream xZYo~ׯ`<&ټ8d> [re%H~{y %v`lvWՔt|w3s^\^9~ %#'RP\fΫ: VMW^7^Vv ViW 6\ ~s.~?sÁ\!k_ޭ`Wjzu5 䊄vo!/Vy˻b0r6n$cG.$kȷ:z( }~(o+KA/\K ڱj&L˻-B|rj_I~Cvm0Oaȗ 6$}/pj; F&y-S5l h4,x/ZҊgA5 :2]M fF!RO b'Q*@[j Y!I+D"{?Mt}}Wv"q#{ |e૬Àx\z` !oL 8#+ 4S>:ra cU`47,:\\BB0Xd{^U>w8=qxN r\З)t~E;U^gL+}UqAј(h}ЂӠc[Qۼ]ʩdwޒ8TS g_]j́kS ι}Qy`)yT JK7 隳={SG`<ƹ~^qhK"5!;.$j~Y1,&(4(`B:B{}hsOJ%7%$)(vY_u\͎uvlqaiYƍ8K/$hkAu34X|! DDwCTiI;lZ;Wg,:)LIgɌP01P9ȷ )T\scK| 4o1#>mЦm^ζ=[,t֟o Xuח'iFPL=n0q)EAƜs]M>y>tsƅ&- r"aXaM@>?ޑUI#w^Oˋ|OwM oe q!M暫^qIT_ڄ._b6[M yn -ͧ}:  g^+bmϬ|1Ak󐈹G:bKv'c^3j~Mz[5z4AXeeCZX>Iu)) U79~kXޫXֆYq͠$skZaEKc*]u=V V_Ռ!7 (}bPoHOıHPq@͜4EQ*lH/(վ=7XԬ8O6=C;914;qcW@Ahpl- MX$]s?Vaed[zD ^Me,>G_|%)C2z_:_dJ B wJa4p?j endstream endobj 1193 0 obj << /Length 1299 /Filter /FlateDecode >> stream xXmo6_o(>lYƀ! ƢmJrEʑ' ) XI;>xG_fsPEh@!%b,f) fA-ᔩ88+M8nxfmtm 1&넂)S-" A)? yo[9Q 5<4!$*V qao>JKJ:9Gg%@O{[XÈE a(XyC*lIzN9Wץ|?֢‰"Zb~qr%ˢɊIEUnLdpKJ |F3cH`dQP88rQr888Q$Pe`~I8 pB %^~G X˻pJnU֬l Ьzm_%d|3p62- "e"=Rua!NY \Tfkw粕)a}ҿE֔;z3e14!WDol|mM.^e;HS7.ze58]8,yK9ǜʎwo^n1,ti . 9 뵓u؎s}-[9'vWwUlNONRAzxƐ(`:cacs@1# 9@;Eȸy+Up7Jn4n)e; ?\؅j6,SpTu֫2asM> AxV,ځ@VR9/vzQ>j:Q}_(-~EM&3(3+zxLAkK+{_x4#') 8= S)`4QԄ>bCi$sw>VJ>ӶjwGesX1g:` endstream endobj 1178 0 obj << /Type /XObject /Subtype /Image /Width 1277 /Height 680 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 1195 0 R /Length 21791 /Filter /FlateDecode >> stream x{_Gݶv{@(BPEE A@WZTjvIZDH` A$CD!uyܙ=d's}1c1Ƙf:?΢m>ӿۿ 'xbԩ#Fhٲ_4u p~_^!j[_tU 뮻b?ޡCېGqss=3gΜ:u1c2O[ny8GW^'}={S׿ȑ#y#~5榨Z˿KϞ=|aPo߾_=Miۦ_>k֬(Dz^7|swر\sMQQQi& ɓ'?'Ł83&n xmeDsCͽ_3f̈_?hР{lҤIhZ76}q`ӦMUczz+8xD=L0h*yO}ƜI*#G%6}y#1Ͻrcaqq|oqqIw&:u3Fene̚5+9PWUq46&G㮩IQ -UVq~fsrq7(M87T֞3djl7_=4R<ƈPJ&yY}՘ 4hPCa|q㒯禛n͑W _Fɓӟ8YjlO)ĉonݺEKMnGO=y|N&֐5o{,)hx nhXCRqAq'~H4C-bW>`zY6q-b9n?9sC=cqcfyԩ8孑_?i|OxW~qE?EX+y8:cƌѣG?>ynON=>o57.4 s%O&ޥKF~W[|"Wfvkf^S^̷g^be~"S$OUɓ'G奡W$TfwIɥ?-ì7K 9?Or>J-(wZL !?q7s^sa&4ob&M9 #{[Ђ;#Nӳg|5Jxlw5$LLx'@*ÒOcg?Y<ݻw=.(&O裏qT _~y](Xs3x<芤JwK[ N>IyMކ +^wuo:u }7hĉZvC߸aÆE}$0hQ̏PKƁ 5Kd7~nݻˆ3)HKMn.'mW&jй\Jqou +~#Nw,qo/'vFB?~ㇹic Zǁrͽ7Wtºu6mڴU9nƼE:.+qqэjxA?çoQ<~a4>{e]vϚ5{MQ6dKT02Ym8n82G3#1뺟W0B?_CXwuo3,iVj?rŕ4hP֛C3Oz#yZ36{QG}UW]~{kԘ+=;jԨ$r3q4͟KʺǍIzIMil&0vS_~ {ϧz.GɋŧԺuXg<6+~w|{Pv{G̗1w!-٧O80dȐKH>V(DŽ8d\ld^W[{{׻F%o*OKʺ䝼M~;N/{2~l2@%yu\hg}C=P1gw'6qxo۶d~vPzozIߥJE<#JN:%$n({cٱ=OcKKvx߿ &$/!>eKF'sۓOr$?9ĸYNO|#b=7pCtnM"h8}IM.e'9o\ͽˍOn"s|uni:Ybucv==)>~ƌeƼk1r0.wРA>h0ИJr*ƃ?ۣ&?ib{rK< Iֈ$0}á+"y^}\oL^>k֬uݻw>}ip/~1tOFtMgᣪ4__E!{  {ً^ً^@d/^@d/@"{d/@"{  { ^ً^@d/^@d/@"{Ϝ'N[d;v۴iSXXxM7=۷ow썪5* ~^p^ds=esw޹KS曲d/ٛ^}ӧ'=z E^~IQݻ7)W^+ {޼(ۗtҍ7XoUW^9rds֭۴iӣGC>^)qڅ 3o߾;p &L}˖-NЇw%{x4"$:th<3f|/[o 0g?_. &{3UVV>ڵK"Eׯ?=x%K}YޣGp YޥKqƽpdo/*L0[eoUUɓRPPp䋺򗿤8ӥ^~/3^8dzw'}ո?{%Uؽ{Q/}g.{㪝d۷on#ܹs&@"{3E&}qƜ:_j.{d{NVZUGݰaCt\[[G.,Ǐw4{ ݟƜ~lo=n=~w+{'5gy.]}#$9ӦMKN6>NӿD}G1{wYkjjz];v["=Cr< ΋=x`̑wqEhݺu94 6Ի>}42{ܩSdz8{>&8jժ_|yNd^8?^H0pʔ)V:p@mmm4fUUէ~:${o8 ;3޿={"^{YԾ}o==^ZZzK/o߾7xcn>C۶mk̞׬Y(aÆ=:JEu z髯3_}R%|pgoXjUE9j'믿+%I/>}7.{9r֭[nǎ̯޽;k׮]~~~TiӶoߞw„ qRl=WVVF;ƚ;w};v!t+W۷3]t]-[K/}wQZZڲe>{#G>>}̜93]@4onyyyC #OJ͝˗/Ooݺ5SwޯjzAqٻչsF8q"^z۷/9{._O7t^,_䜤X4?zpѣNj/d/yyǮ⊼{ً^%e% fc1c̙x=yU;e/4Cc1sxqqW{1c1Iᲇe/Lfc1cj^dc1Ƙ&ً약c1^ً약saΝ+|x~~~=VX{{mqYq ymm.]="6j*vҦMg={v?.x…u73ƪz`\+Wgi {DE6tO>Yn]dO>`l߸q={zHۣRGg̙֭￿iӦaÆ~us3F)-Vm| ~饗⼥-[n肒k[^^y@¹q9رc-ZMF`v=˗'nݚfC{o߾{!l\pa1_1_NO3o޼g#WΝzĉ8Z[[%(:9y@9":[aÆ]vE/l޼ys^^^~ilh{ ڵK6>sjglժƍRO5kƌ?hРe^Pr>t?d/;a„#Fd>kYZZz%’g{nݚl_hQn7{^XX}pUUUƱK&)Sdjgꪫ͛W^i({lٲ4^j7'̻` ]P5j {H;r-+W\~ٳyzwygqqqo~-))5jTd_~q46ƥ>׮]yÇ2$=Wsx;vD8wҥnb9s@\$9rH^^ޫZ^^ /e]  {;vW\Ѻuk楗^J$({ :wI*{j?>:qģGƯz֬Yv۞={bcMMMQQQ$cyѳݻw3xXv۶mGQVVf…qވ֞={DQC rd/ {Ad/ {]TTW ^4i~ S^^o߾Ǐ^di!(#ӌKsIoEEŞ={v]SS#{gZIY,2{1ܻwoW"{4m6~1MW"{4MdAQ^kꪪ}^dmNsʫ /R"{eog7=V^,gISLu^ً약x@"{e5+{e약^+{e/약^#{e/k {΅h,2IR"{ϴ%^СCN3mg!K(#ӌ[UUً= vW{48D6a^..4* ^ ^d/E^d/Ed/ {Ad/ { { ^ ^E^d/Ed/ {Ad/ {=*((3fLEEESĉ555Muvwq;rȦZ϶mbIMuv7R1sKo۶me/^hٻvڮ]&?>};v!X[[[ZZګWC[Iif%gzVXѻw\.],X j*զMzrʾ}>3qtq8NߩS~:9M\u '{/OذaCqqO<|u#GH3sK/Dرcŝ;w>eֻ^z?nٲeuuu ѹqK?_/۷p9s渻@…VɧZ+{EuƁ:J3üyNgs΍=qD͝zrرc[l3Y֬Y>guz/d/\/rNsLeqv9B7{ذaC駟GOb3fL~~AVXMΗ?8=lٲ?P6t {}o͛CÅ۷oOWUUEƁݻϛ7/=36Ҵ@ÇWZw.XE7]jK.ywѣGz/d/\cIIɨQ׭[ׯ_8?o7رcҥKFƁ9sā0 ~9ꫯ q8${ȽѸL?4{ӳ{xo/.%Ǐ_PP;qģG~s٧zK.^xqzҨmێ1,~… w۳g˗ǖ֭[r=\+?qb15'eI^@ {A+{ً^%e% fc1c̙x=yU;e/4Cc1sxqqW{1c1Iᲇe/Lfc1cj^dc1Ƙ&ً약cΔn)#{Acɚ^W^]^w+5hKbm|uSxѿ"i_x}Wws迎^8ZNį}Zhc {1I'mܔTd /M}P#{}vӳmݳ$_RV2aqh;nN_؜,hI[;J6ٻ&R7yeuC;9ޘ>D0 c.鼬;H2IHHSVOXmݳ xs+ìҕTbTrx.5y27ޛ޺)93<{'ХCwcA~1F5Ƙ End'-.zns[o-+Xo/)lx|5mω'6tV=P^]>dɐؘLCv.&]cil,]N;EKE_/r~AwO_nu?b<cd/^cђ8:1p]s]ջ^ts=KTp{tKmduձ.߹<:7yw_js6Ή_8Yz_>zjS e7_~,v#1F5sպ1 c5#{1c1IhqEi%Kc1$e/LYsȒ!p1c9w%{gyٽ>c1s>q畟7a0..4* ^ ^d/E^d/Ed/ {Ad/ { { ^ ^E^d/Ed/ {Ad/ {?E]t>ى'jjj۶m4᚛| {|ޡCv-??ѣ,3f^pg޽{[hf͚%K/){@"{cΜ9z:qĘtRPP0z芊تU(6mdVg|uҤIޮ?OqӧOرc@{ѱիW>Ȃ 2^.۷O?-ZxС8wߴiӰan`L/]4b3y履~ڲe}Hԑ#G>>}̜9-**ڸq={zO|ɺus|dI௿;bb꽬m/8W4V]]8 BFF<~G_~Ν;ϟ{p4/bC{رvڭZ*Ϛ5+ dI{/]|yrx֭[h͛l޽{}Λ7/]LHݹsF8q"ֺ_7tSlۼys։0a„|0wʕ+FכY;ܵkWH^boNzƍy7lؐ.Zf͘1c d/\ zWWeɒ%-[֭ҥK_|Ŕ)Sjjjި˂իW_q3۷oOWUUy&nݺ5پhѢXLꪫ͛W^IPe>|8y ,}r* '{׮]X^^n9rH~~ /SO#NyÇ2$-$37ygϞɛp%%%F]n]~h#7yQܾ}7xcǎ]tIO_eu{W 5ñ@&M6s=g͚UXX|mٳ'TSSSTTԺuo|rEwU?~|AAAĉ eoUUU,#۹sOr\޽{,o\rez.k…qHM> '9d/ {Ad/ {]TTW ^41c1gzUwV픽޳ӼCqc1cY_U^dVRVc1Ƙ&3k1ci)Z\${gc1cpd/Wc1#{e/Wc2/~Ɏ%x#ҝ+띻[Jl[y[l-VOT^c+{1|GGƁ|hͺo71^ً약s>_]qE_M>vrc약^k1ܼ_Wo#{e/Wc1#{e/c1#{eEc1$SH"{ϴ%m1ciyxò{9dw8c1Ɯ޳`w߼]j1c9gv޸ϛTTd/^@d/@"{d/@"{  { ^ً^ًd/^@d/^@"{d/@"{  { ^ً~UVVw}]vmժW_]ZZzĉvqY555,]tѶm***Otu6]\ұc~;_{cnذ9:u4zw}wƍ?|۶m̙sg]w5Fץ+++g̘%E0`˖-eeeW:|Я_o^8gw#GL/:t6ݵkWN;vqywusd/`vqڵ[vq<V\ٷogy8cϑq >|x~~~=VX۴iSoBW_}uAA{Сt?˖-+**su-'ۗ/_kF>zإK $ZdɃ>ZgUUոqڷo_XX8mڴ$NzrȹզvwXsd/x8p * ޯGgϞ=r8~اO3gVR۳gC=Mϐ&ۋ7lذnݺqkݻwʔ)_~ylc'Y;{W>|8O.OXlcǏL˨1c4(yeuEf7mTv6h[zrȹfm۶m>}}(ߺc_|td/xN0aĈO)^r%/Eefofe-,,ܾ}{hifٻe˖hdx˖-Ҭw?űwy'G,Vy,XТEt嗯\n&nݺ59hѢnݺeކV)'Kϝ;wȑqpdoQǎoϞ=;b/3gN/ 0[RR2jԨu/2{]}7n;vIާtҸ=L?~ȑW_}^ñ%"3]wu3/(]gJ(ߺMNzrgoC4lذX-[׿Ol/2@"{޽{(֭[_s5/R oiiiΝ۶m;bĈFfo\o~ĉ ՚zw…ݺu]7.}{.NWdq0Nw ٳg -2̙3'OZg,{~siZO9׻LQQ7kѢE~O_~ѣG?@"{Y|w[lRÇ7T75tz?WtR_"ً=G޽{ذaMO>dĈM?Kud/__  { !{++JJ/f4Zyy}XTNFYł}"tܹ'7nÇOMMd/ҏ]vUUUyfz{{ 2;;ӛܼ}vs 2{[ZZ{ތhGƾXyȑGill=Z__G'[/^XYY q3=Po߾d\ZZ:}tP]]V@ 㬮˳f́4{QEYz 2{WVVJJJ:::fggo߾\<{#cny\T^MMƄ{c$m1;!&''#lc(8oSbVwޝmnn.2ۭfoDtu;qĉyc $$455,ɣ^x6fų76YLG^~=>7&d;FFFb91Y_WW`ƍfId:vK.r"c nQ_^^~mzz:v ^ { 7D8q"b-. 8oxM-ݬljj*> &"w5<<,--H@i\,6c"j}v:ɼܿ}}DsO5K 2{WWW3L/]\\L6K?ᖛiMxH׊t'KKK;fLo;ۻq[hfMMMi}3/))I~QT,^@"{goiiiD|],ǚج=&''_<{*++cڂ FN?{.\oQq`ndc9yC ~7~xt}c({^r%o۷GNx~=v {X˱! {̲7DKFڱcDZcFGGgolGGG ?~jUwo;sV}^3 {AƑ;v?P333oi藗??/V23pów_} 3^[ ỳ @q;3gΝOqVe߯y@ZG[&xbe/^lIee${N>\]]}Ÿ+&*G-//eee>Nܷo_ܬjmm}\plKKݻ+**]f+34GD%7/|gG^'76NXhu7J;~~4ZP;cuf &Ra7!fo7~XX}rmflbS_33?;2뭿8vsI&eϏ|֯nkv?L6[uv(7WϼT S8L ז{d/'Ç#0m7ԩS wޝmnnN7OMMϟ?>8/WKJJbeOOOO &B;=rH΍Ym:7W23KZ'WW"gkzO,ܙ[ɡ(XyΗ߼{Qݸ۷k/ج{l>°H:81پ]Spu2F~+v1jKڞqxoN-f,yv~cdo{z_:p#ٷ Nu)x~E: d/ųwmm-Mn D;88,Ik;==fb}m"m"Z-ɉѸfg{7 KIc&b9JHCvYW'ŚԵ"_ZmF=a}d._ިA0v忮s1 ٠gl!FKoee-//MndgzLM&g^p=mo(eW @عsgEwܙM/l*--M7}wH5^bgϦ#8ysxݚZrkCӋ7+}O,n)>9O_Xk-wfꝹd/֒cɹ܋\3p%7ӻG_08290M\ͼ,2y8L /{?d/^»{رl>GURRRloej r֭d9>Fyf 2V7Byh':OvO?3V 2O{7fleP8.={{E-8[m+u9wi%'yexvu-F=Xűrza%oNm~uye-wHμ__?{;{Mo)8S8<<<99Dɓ':T𳽛ez._> ѶSc䳽G=|pގ~7ɫ(O|%ubvyħ_i5qWlPwyvUVVwk׮'#㮊orޘL!BH1=}tyyyfݥKf M~g|+>+ÞQ_v5"G xֿBLHy6,Ͽ7Z_>1ŹAZp;s˱cT^+y{>8@rEƣmbJeKύV[<y\ыy궴ok {D?רȂߡ]궶iZ6pm2&yc^PЮcU7`i%9Uw?Z|/ {`K2٨j+ѿ{gُ'g_d/^@d/׿;@"{d/@"{  { ^ً^ًd/^@d/^@"{d/@"{  { ^ً^ًd/^@d/^@"{d/@"{d/ {  {ً^ًd/^@d/^@"{d/@"{d/ {  {ً^ًPa^@d/^@"{d/@"{d/ {s^ endstream endobj 1195 0 obj << /Type /XObject /Subtype /Image /Width 1277 /Height 680 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 864 /Filter /FlateDecode >> stream x  o7x6} endstream endobj 1200 0 obj << /Length 320 /Filter /FlateDecode >> stream xڍQn0+hKbo챢r8DH8Epi׳쬄5H F9A`Bu=C9/3xޮEFa<7$2Sn_z|VmXX^=SQOGZˆV> >> stream xe\MgFADT 뵻^[TLnEPPB}?qw"q<fggfgݽYHTvB|F!BFR @!+0A!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!BƉV @EK֭BwX rB!d0A!q(!B B'rB!d3VDIW[BbD9m5Jٽ!BAsgN;ͯRQs!TtO|!JHҊ`2ȿQkuy{y!PфrBEkubXvIf}8&\ɚeL[*i:m92FPR, ž0kQ)_2 P6S6tUeeOW8|tՕ4>I\az]2>e(.w9CԄ*f&l8-bW_Hת uK/*!ʉmq[ŜplR|E6fWflf^R<,bcRۑu:wTJNgR {|蕡O4̧BDswY)@S:,T7U.)̥ɬ2š)dd3HdMg?S4EL9k2MUi^U64V,}&Q0ıdc +$c2IkimM VIw: Pd QΌ$$hw+[uITx.MK 9`@M) `[c7|NPȨ39Pd QQvtf7RùiSocS{g*dIQujڭSw#S>9irRL02+C0"+W2Kx5oLpńu|eۍW2*t9]Q'b%lQ4.]8Ǔ jO'l* @䲄+_LIdYWd9Pd X[>^6A󫩵ȤeY/F*h8]!-IƦ,Hdɤ5|*hbZKB('q> ӉÐTH=^.c6e #-Iyca|,'1 * Gõ|^uoƕ[xcQׄKJT әrlX?!d$47Ι] A8 mf6 )Co/՟!H/ԋ'XL ͻc>7&s*WaA! NQ<?*Q$]P3(ǻ]˛aϾRKT*Ss,9}YpN@FqJP.(Sd0Vs'"* F텐Bua{UIzX&\ {fՊ#R-`!Org$dϦpZn'!] >餞K9ki9${8PHg5'" oX2o;㩐~(brYBS' Atk;$~3 ,7#Tz=9_ cLGy/@( sUY?5+LxGHW:|F`U.1ė\`W΄,M*s  JKYrB!]X i#-~^ѹi.cxnғ+~ki1?,AH_o _47#T$Yc\{ZTi1YΩӗ;eqZ{1kpyJ <4 }lxd?SdnMvtf"qJǩYn-ܖ T$0eB*6,J1k95҉o)_ 6eL#=nHRux(=u4`@;^Lݡ+onB!0N.Gv+B!>c4B!>F!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!B B'rB!d0A!q(!B B'Za7nB!Q\|)?,,, !Ajjj~wB!d0A!q(!BƩD^n9/i[Mا2:Oc!|穤Fj5ƍ[}/L(:ۘF3?Q‹$$L˷RI;##*R8n&9*Ԋ᭖0*j-*tag<[z] J.rQG+XJB2ƾz8Ib $o5oGS{/NDSMڭ}״- ruoܷK@hgf Rqg6FyB䱫y%+\wDP'ܗ:0zS-mmU_}-:Vw+v;bMIA@Mf!9 9ɰvg$+UCJnͩKNŒ rFЯf4By]B2]K,#2Ҍe}KjLp[ƺ3xɏIilߗKS%yUaR@!ϤPs*t޿vg`*Vv=鱚$c|e:vn~ԚqT[&.}tP+AY:VFژhH{j?uf=jL_y٨Nj\q+%DJb[yÅ#T([1EJ/yץ?:)| \7R);!fD$ yֽKRvJjI;}2ҹ;TΈ )JjaFMoNrifG6UiՍ om+P}L1u;]hzG_&!/K_O2UƖCOމApsZ5*g[XLPيfɾ'eۜMƧ*X8::gU5}ki]PP_sZE,ʑJ1U+2TY*v9PS<jO^J(S{_T$ҞY7b? 8Z(1\sJ^`8 Di#%/Zd^ڳj}G+pn`9ӫ,>~yaP/4;[)^!M6ZqKPۛ \{tTFf%~y/=QmyGB8<שqRp̭C7tsFD\ƭqj4xs')К3~.fy4ץ YbXGυ#Tt(g eoJ(l3=QnV8r8 ´91 ׺&Pׯ ˜}]Mi]eu*VgLeOqZ0#).Ɲ)_N@6<]Y:^d[3nSߙ(gMʊE.yH$p9ܗҞMuY06?$q_=AxxwǣfNlymNp/}:Ӯ+2 SvJyyx,JF:j?W_#Tt(ƹrϯ*{̈́k42SƷgbIsI]6n諹l&~iiwEyE@PO8[c,m;ʴ%vuE.pL̦Ud n8[nWit;'<=p31#%) T8BEmp{Fn;\*TDiT~^}&2QNg ár '\KNjNAY"~bJYFj(*^ǵǮ zud\ؽ-#c²zsU%wy7w7+kFWJ_n6i]U߄rͧQeT`\KKM]Jq51}mتL<д[Eϻ ЬLeKz&_|י9PQBȘx;󑛁laW[ReG;(St[ ]}Fz|s|?|(& *Y\]]'jG9_ R&ZďF1wߖr8n?ɷf}a*tkuf|Je*gU{\-s+\I{*(GeoRv3Z6gnݭ_wu2]j\^f{У;:wլ[c݀!KNҷE!*:s΅h^m) O~;G?3)eT’.JL߭%M7:X^-LbQI\[Əvٟrd_5:wwﻶ2lv7iTeHrfG)+;k7ڟxWϔXum]]`H]Hx*G ]Xt[4κ^MȭAX5ظ}j!`wljCtҎrnC9Xy.h@Z>xn7]Bƣ?~muOTڨ ;(c`&튦b`#P``ĝcě٣V9wG~q'žf~o$ e/)Wf%&@-Zc;wn>y1 fGHY T1{i^9TU㈈.=Ȋr̽ 輀*W"sIog=B!T&;NG=s+rxУ= \|.B޻ :r[7k7Irm;^|.p^fW*22P|XLSSnl<7R5Xĝ.,8FYpuuuuuv@ [xz-<ݳw_~5xTLEy cB7u$F@@dfǮeiqr41K;4s,pҭv2.떓"[n_on?8=Z(eZ488B /ˋZjϦ60EQ"ͯR ;,wR w~ؽ?DH~LIdjS9AUZ[~Pc78̣jvzVʶؼd}gdv/$O XBBB)zB?.I>?и?G"C"XAEņʠs-5Ni=jne&&vܹ);?|Tf)Sk_WI{~+U BgVtϥ:"Tby雋kvCtߜ"S,Uu&F ][E^ď dĴޛq/;8ЮĦ ~ފCx58㘫.[=FZ[v̻'Cn=nS'&+D1; T)5k]Jv>qU7VS°M~W7n7FZTgG=kJY}7W0[JSóo=<oخ_;-/VC8rT^2mM:wo߬շ2?;4kK*}jg[Ilscws{^ǞT#J3B eOlRjݶ݇ =jҼgqu[wo^^ya)}ݫ CwI)Ilo(2bwN#^JΣt5=tyɓ7P4=~ sΜs.uVXka8rHJJ[TYlCOjݺïkO%nvAC==U2:Qf-"?(U?GZ71˾7x)SsPa7Ĥ6=\:lQ;3z 6/?Bw(?hV\q1M773`3s䩣*SnV1`ն}B~eE *Ԭe@%KnU=iӳӨbA+gow 薥GGrMw ^ PH{ J|qXGدOZ֒_o~Ǥ@F@;iO&7cID*j Z:8uuRXC A@q-=cQZŮe5d+Yu>[r}6沜-ʷ!p,xy- R|:xw {A>lUg #]{կ;\ Z^k)GҼwъ_7V9gNp7gm87 =G5@/QFIs/4 sTHTgw"90@g2H?? (f ^x?h}Bk8hip[8G|RR!T%r71PFeaa~c>z mTԦׯ֜:3p㔍]楼\)(U6Q\ d~rw_ J(~R8gu!jR>ī>ܻ SG5=vl7a/@PhM TgU|8Q-֣x4C ~DL\@4#V}aVZ);ʆ^0q@IyBp$ q02 GY+eI+D3&@7+(HH :hsբ)uoaT*ݞAYL)Oj?Oll"JIs2SUth~ Y%LcA] ^z OmUopJl`$ZnqQq9.~,Z? }>Ok2A)ԮMw^-f b6 RSSsΔ;y'{D+5ԬhOӫaE/̝`5]CgG|@z-{YJǜZ5h:JY\|lYnrN\ ?0PξM@BzQGnj1|,mzC)jf.cO6MRԮviikCzO?[1lʂqN_#a޲ϒM Hٱ537,tsq\ެvGUW^vye|\)Xdy6eQC.=j&TJVollUv{In Ç`ը$,Wԭ;mƆ"~\%~U} 師a0A#XMC*fl,C{TReܜrꚒQ((rH$ݺ*XD9y"xΡRVW:U2Z%ht[gIv2v *sy((.օsK;[׬|rJ+TP6{ dsvqǖztҶc E15 ^a/-?_5V]QiE P#]Q}NzhlЍPIPrA bܛr=Y> ZiѬS {@ivTf}*uS{C4a`+w#rP>jЪVphbk"T݇9vvpd v>Ҹf#oejܛt|Vo=ץ:O!>##z^;~&7. ۦ~wn̥Z-{8̍MyMW9.+LaP*g-xYsK SnSGؗK-{[q+C&oe ;ѽ=^l9(UT7!g[m~բVo]O{})Lmk$i۠ձ7<~C7N$LV/ZJ7$W,+IRe00FA2u"uV#_\KOq?7{'+çLٕ/XE LKOReoDTrʞ*`. 4} LRc{+0Ag77=::rToxj sXؕo> uä ߤfz)qWOZS'=حZa.MVQC30IBH9 A_rP3aO <ɐ(?Lk@$_HHsA+={~eˏ/nzU/c3~ΥNVx/]6m,N4|-IZ!_2t{U8408IUc3I* Gs_c>rQ*he~ HR%lscreqŨvqmzZ,~c T-;`Ffs~KG@`3Q0dجt !T4Jpn_y*+;wnW{,3, @IY t*ԤY:N}AҎc5n) {1sO.hZ^am ݿZM5CY}{pN o,o7g v/Aa̗ PObo\]|ϱKoC&4S]W^2}Y ϯvq@ţXR^+ֵaK.?xbC<ƯmY[` 3(_+w+ zkH`k ۭϲvTᬀH/rOGU<_{LX]M)wLG<1Q߂:\/oܧN7/S꒭t{Xw~ИfxA8u.BNݥ5;nG% P1y|`>C|s ZX_E-]tرUZ4_/yb-;&eœoG-.}mmt)Lx=ݿu'l&T T|q̩|?~}~Uɂ9&I@UZnt$i77..#+mT&qIO.?}٩}mեsk/oc<2~[vݎJuM:U+BBbB!P QB!F9!2N$9[\g]/&ևPl>5}Q0;h6M!:w[v-&pw0ZDN'3N[[٫8Doܐ_5#`8C8I)kOzDf@;{ uչ)V+$U1߫q~B?|}}`Æ &&&3kjQ b%Q~y_P@jbA }Sx73]`>&36~ 4$atdVĻ YKYgB=L*rV˖-Gչ,VDa@+kTr!B(˗/B۷۴icx3/H3!Ո^24(ũʏbV°V(ݘ*ɣƢ:1a2i񌊽mA_ "y*VE 37fE%fʇ)!PIA//<+άm~??<)H#GY@zcu4U,=]̡l˗"YZgӿ;7MTJ% _30ho>VqJ(s=Rv沢&)*W{=HC9hM6-ZP}ٳgzr֢US}7j;%K0܏RF~N{yI)fC+*u9%X5 37A!T")=k8BrhjViw# Q}vk[I.zPME-:~…ׯ_8p t? \w%I33T"no#X1$GH5P)@(mG"ly-Xg JtVrBdoev_@*ǖqi5/=O|UB<_N Տ#J 2} ҢDL3y-3ZtQRO 2UHrE?+bA_oo;J9)gژ:[f+Rv_ ;9itwm_#~p,@Bd̗2K7SSJu\>f5k\X` =Y%yOv+P܀Aܪ 7~֓C/s}Cǟ¤"}]VwV__f7D+ȷRCB\ߨ4K;]!T~nh7Z8BLSF@!G!a7sc. [ۅ8^#K1v {]ה@JE_A*Lͩ@$Gi]?aŐ-l^/M<Ɩf8 [1=E^|΍]-ݮ}۽|,E0s͒:6lQQAś'5JWHb%Nb"[gM͙m A[:ʰ9GGH ٲ4V/aN%+L5xY< M!L]?UO^f9ŶJY>5I{F1|a$y(XEȈ޵1륺*U5㏞~4Ҭ^Dݺ!Α2PljnzOcE*D릕h! eMtr kw^0?u DCєu*KҌӧyN::k"[Q|)kxء!;mY}K`Y>rMپ-btZ%d )=o=zs}@rRB4ũq5[vj]A+ Y~!oY+*Wgv*U1y[o^-[vT:dp%(rhxkITdiґn=W }oi*}SC]kJ]^Qy|\_rS($}ᗁMiPi=tvۮáؗv~GN&@PM)\|rbcc/^rdȠqѹ݉) "bؿz4 W1<\%l؈ߤ 8L(*}ĖqC~Y׌b՛Tn|ZMu W NϏ?]tH}-g*uWǯ_#DhL"J>q2N&f{<1$iWI6AdM@:B_$ʶשC'6j5>cޒA1SǏc3gEpČ3w?`yNCh"mm7}rSfk~*nRu_`ݨY}Lfr4vrΕosab}& mW!z77T~R}LEs@%NVwq:+db GD'+v4L)˗/B۷۴iۥMN*r9Ո^24(ũʏbpq{kX+ nLbpbQaxF^Զ*`.37k'YTb4[le 5˔18s٘aR`bkeǸr. ^Ux#]Oe9g|`–= ̲*|OSzͧ*L9NAʿA+BR ¿kvjUerB&43{<5L)O_SO:~RVQ;KU{~QG99V|)}ٸC0ʶlmN.Irc߲;*rv89׺O;z+Q>7\){ =t\U6]O𤋮TA=ŋWsqS:2F2KkTe흶l @+z!dU!gӦM-Z\t zY^|)D49 @Ri0//hrN}E栫 3Iu ӄ/"1)eΠ1%c;]= 4GOUM3KnYퟣ3@淭g vhHP 4?QG9xʬ]֚ ΃ ˱ Y;tڨ.jc6xi0GNdHk,)~dmv //܄8ХwsL{XX!K1hg1hQGIQ-}>+g)_^+GՃƏЏvrSSދ`MR|F~;٬gژ:[f})'W<4^S_FƳZ{McB-O_|5Sح@YpwV^vbkUd4 ·#QџA((B?I |!*!<)u"B(<@1UQλA;ew*;r?`0QڲCeV{y}b\靇#B[zСꯇBnϜlR9n37^&4yUp'4z}(G5pH0T){.qңEf;ko!23.g(B!B%DE9/^rerrr|O̾HϯZ_zFA9 NmϏ? R+N@Riߓp,TʯV_FH )jP; R/; Kp%RHk3;B!T gV///kɝ#""ߩ~tf1dW/ouv:vmՅj[TئiL=MUE &UM" sj [KS|ɥi UoP|W+ѰcF3HPYB˗/B۷s\\wn.fly89eaEW{Ae`ʝ"ҢŌtUQ+#ȩShrK\#8Tr9ygo3 BF)+79Cc3?K~6w]G(m NQ̤g;7?S73)Zd+[DIvW\Dv YGڐ}v}Mv\TT6cQ4]ssw| ĆAtG/Kth0\9]$B'm4|2}07GT=D Ǖ]Psds`ԘH$wwwݻKE̚S7wZå0|ٻ(Hly--Pso4V7vj'%P+om?SFu۪ Fs`[@ 13C"SRR^~aNITf–m+yW\L c/^cR``B}%Wp}(*U 9DJ&Љe6,;QMF#6TXT@ Zj[Fe¬M {ᜐ;̹cy; 瓔֔I=Dqʜ)[/& bEb`? NmϜ9,:Sl!;]ڮzQXZDV"w1AT9D/VZ4Hڸo5XӮ]H6'KFMVըl#aN^̑2rN>fײL]xs9&tT?Q}&gXs7Sr/ɠE9fj߁FP~ۮ Ӏ.}><;@k}5vy)ߩOz5b_qVwMWS$ 9'ʲrX1Qg |u?4\+o"D9I=F|cᛏj؟՗e!"i@IL<줺*f={T݉XW@T+J 2qg Y5]:7}[#:> =3}MoٌcpnN^RTRͪ&,1/V֗FoMv{|&ױ7浵0`+ 0dn:pؖ^k;(niI*e na" @Y@2LAGS~?vX, ^}[AQi'2ԣcQ$4* U4'sq Lq P.u~7op2ӆXJ3{4Ke$F+q L1Z_6e 'Yn9~xTR?˥v=r@ Y`RBug=}T#G|ZR@W}5v:Pq!^RCbtww˻);:p? ٔ} Z6*SFGtcбܴl!W!y4sۊqǯf {mTӽBAáM^ͳ sΝڌx݌w;ʅEޯ^&g: ܼ/ZNGy|yiustnOxgtLp~SqJRl.E$ĢWSksb߯}[8%ό>3=}}eJm0 -ԣoi fvQːELhHjk{\"@E!u N<oO8w6?">I,s⏝:kZm K,2ER 7Zp}>J\F ONv87qڭg>fL)⸤݇ZFA7:fNSd#bE;hqL ]`m3ccr؜TF24 1'+TT[շyZGg7|C7t׳::>RfX``VwxS DG"iz.V$(49jch}*SU /Q :  jykPP9kϠ#|gnM$ GZ+=94DΛ4'xIvO g|L" ]:xпC\XMMkˤbvQ#PddX483m_KY5ZࢧY[]' c:F_e T.nu 3|J98ɲ9W(Xre$Q x;Љ d40+"2d\3Ru$5 r@qnP@.Ԣͽ$82QѬb+,2ɤlN@&2~kXLgVΣG-:LQH =8+KqC^=fv@dhd[jn?ٍjfO#^1@fb+̈m>u8ɼֺ?f,/:$! D2VwM)ifLKUڄ9_p5gƦT 3f={F=e~/R+oT| ÆZv"Q=O1 \p ;9H!F5uVg}g* MMTkAV"z~eXMjxO ;ҭH1aѸCh7?2`E"x#y'|.,(kx޽?t> f'w<|r:*r]NiY[3q7docR= We>|tem__ry=oOUϳؑ`x_5%`v@.]\jFu\|<&&&&F9aaSZ(Rt,H@ScǥWoJ=0v㹯Ƌnݗ%E|LbԼT+3 tvWʡw/W\S;gZ1"&{!lja#Dלfh:)s}Y\9ٷORbcRt,ݖ+#NPcNv;T˽^m+mE &/hQG'n  EV3/Yߑ 9/6J@9,kڅ\-a'ΨXUXp 33ԊҦVG^EP.}9àK޾ѽcNNN:56wj>_ZG75jݭ&!+.>[*3{ p*rY]L>[/"?G_Q48ڲ^]C@%؂5w|e^{p7/ֈr@1)zqGz lQY<~jÃllS5=V KFR:vߞpډrDf:gnFcejFjÃllS5{ .*짴0'UfF?Urt;@ Ǡ^Nؿ]bl1) 5S**`'fhO2@ ~%rIt)Fnav鑰]'/Hd9'kYȀf&TkվTAתҨ{n/oцv^9^5i8{\[ޘ֢@ֈocOraGz F2MRqxkuʩX%Q]X!QӐKzm@k??AMdg =Ko2m2z{>-tc U:p>Kfp`pekS?o@  {3RNߺ{EcK߲σbΓD=O˩1~kmHOƼvh0ږ| {ͫp@P\LBbՑJr%.֡qov_≠E/-^κ^\| dl~ZӋ@ Y~]#;/{MWAz9k׮ &#J_Ǎ.)٘JU\KҙCm]]z*O` v*P ̈ $k}WA`y_RdU\]sȕʽح; ȕp1Ai(NCxN } 5x@T$Dz3H)Cٙ8ѬYY霼o:O&/A)!lLS*rrr 3gNlF1AX.Lc7 Qfzjwl Y!@6&M IƍBd GKQG%2_gn@ ;фryoTqq oN0bkCe -.¸ļQSj@jfO#^P.L`Naё&2zk3 Uq._͛75Ma-+šW [kܸ-D=&v\—|=U )3Hۉ*SS Ȣ v5$;0V|~QCÐ>.GPc 8ue\-!-QR费6]9lN ZݫtiBe0qdɷ1;pCQqR=vo~db?D h|PM/'XkS[h SgM70%X,b4s/|4x.BӕhuJ,$` Ě(aDnm "[40dS |dlëߑ俐>!(^!=_*K5 N;YUȯ<*zLn4[ǝ 6?i[qm!ȿt Q]h3~Po/aҮ%-@*y*gezަ1,%0-R;NtU-ݾ]m: gM73c޻woŵF1" _;aWLk.PDxU*gʈBʏb*HvB<uX5 O[$h] GD g,W6H6Wx@8bKR5P#uiVXx?K$m!O$ڊvсY8ͦ[>N@]xsn{s)Mƅz&lUO\ƿ"js iWlxs;mq3Iǃ pJ;Wg$Z}z%:ݔbX 'I8w%ːyg՜ Cda]1]E5&/r@ _ l(1՜4lXq7MYvpW áJU-{]RΩ'rmxxy{ 3nAXT s g?wcgg3v[=_*f?P]+ҦJ|}z\#Oy]2Qp6 JGڠO,8ڜX(U;8cQ]Wļz{t8^!V࠽uD/WN>wϟUq~e\ V=mOxr;m$-ӫ͋UM>wxꯉ8s~Eb+* husV)D578TT9j˥4#޻OWKWPuurvmJt @gM# ðVZ5mT՜x5#mSWkwv+(>;3ۥ}ᓎ;۔.*Ŕ )̹s$`e_v@jGĖUÚ2>qwy<!w Zwd,;DaL_"i7i++cle,U'7*y{xNa{_<ײ%l;w,ةݿx/_>ϴ.' ]Xdړz젺VQ?؀C1qZ9%S$[Suiw%.j6W+Ll$!vyk@frm,/\&,2TZGg7|CmvS-5O&-pfw_Aѧۗ۵)vh*>kڨ4+#{7o0cà 1;ۏX'S|;rODӧ7M$1LYz\nz$*$1#`^&\ԃD#|Qf}k̛LIȢd&q{tVuf3ft2a#w_|7ivO-l2]8=9Y6"5?q7ɍB'B۩K-/|г&QAuM\o7 HJ5\[sNL?ZOOW:%m(]:xпC\"afmڬ.'%l[Kp6GLY2Uj*0F3:4ƐI/yQE=++zIq v-I Dʴ5>|qfp<3Wn5͋6t6 t_t"hיֶ޼Tb5 of6Tȥأ͏ZEUXzvm/;̷xae~4bP/9  3`VDTWcf$/'c r9HejIr3 遡gY=.sȫ &RcBM1ٱF1~ ~Ym/q{(3 #Le$װάG2[ujՁFkk@dhd[m(PچrwPY#dRG&}Y!ʃlؓXfLKUڄ9_pQ-?Լ4fF ul -Dd}IK}=bVDR}b:$nwoho5v`\ݵCs1CC1qdAT^}X=ƞғ랕x dej~ ݻwg6ngP"@L~r9׍oysY @FQgxPFu\|<&&&&&FI-|X 8to6䜏{˿QٸV۔V-mQA &>)tn08hira0.*`v@.]\jŒ(?l1H8W?aw V</m7Uu䢂g1>E}\w EkhPޮE6ymY&WafffknS=lվð5Ca3k-EjLťAWf@ɋH[ilm]{.]+<֪}9\Xdܔ5 7(NCKnߌ iro(Gƶ/-§v|^k2G^xFyjes??o^}vvl;!KiMК$]l,^-Pv =aQe VZ*)hOBY.7ifs-:mekכg,)|n nU:*|:/g Uj10tSMސV9{o =\[2d톭?4zK-O i󈏝Zɵ}jNiTU:/ޮM h{;J}֖WU.77Jbh/y !Qdr1NQƓ8:N ϭv(=`jW:Tk;5v/h`_?ԣ;sqJZ;}Hg6%>{ˆνaAZ&ߩG8&_3w,9Mty_&8!}pԨ׈yj^A= O4r#M۬M;W@X?Ř4o.U|=(M㗿1 euawi_5K8kBك|+"r 'C 9ʞ@^5mWј`o.-:=8סJ~EjalA|q.C -eFhuɫK~#N>)$wM~|x9YYYNNNAAAsi߾=(F1!X@µ]8z??M()ww NY32%;މp#U 0Հ6X_ ڔJ࠘t4*:9Մ  85>ͼ<()l r$F!2#'b_eF]B *y:scLU?jWј`:r>x<A(rg#gQm\I^ )ȹ'NZdf'xoZ啽/ي?޼yS5Sz8LxC|j9r#ݡ+YfO!N%\N HXBd">Ӡ`pKq @]L>[/"bv\—}z:(j[reW>_1M~@ Ã6ɗ]d2K b 2kbO8})Дq8;9HK֖!ՙ6%זu f> ɲ][L)rΗSCP(6)#R Ӣ Tdϊ5p@>՞0IlZVYnxGli8feϫT_$)111%%ױyL,OM<\9hiCK9iv+lva]Ή)ܳtFAl "%D6K-%UY76Wj;Sԡ;2oU)'xwV%*_@]]ZxR45@ cu96g[MQL:@.1J)ȴK;bS]urֿ+S>eSy5U'ЍL}`MкC=kOSF3lXwY.P,ْ DO?k$xNJ-U)PZ3VFg0luURT7ؑ$%w:2E&7OHdUFoMn~X:a*TvМnA"ߟdDМniB JG>srdn`W;]P(NPjՕ=ou_wvsGd91v@5e&*e@ $-^_-h3q7tځ)\:oדn+G\_ 5]E5rrrRKcǎgϞߩƘlz_"ӗd"IbB񲄃 O 44/YGuK9#+><v*Ӕi?zrG2Ĩgc)31l Y51#B ` Pz(}bQ)S͉8"Nߺ=2\՘_X@b˗/+\͛7uw&Ԗ< _(oԅpID*nCE#<@Ѱ-6{!aB OlKϋiG(Mr3{$ 8P&H%_ERc-uU>j<ϩܰX Q߈Jd^μZԱdc⛏6-sߑk4+mɷ bܺ<C9@ ez$*}ezS1{V\T_L,+1Or\q ?G/)( _ p@ 5~BOlE$ D]1;'!UzTC"SRR^~Usi]r0K-%p b_q519 @  3+ѣGjfO#^PmL`KЖLOUðVZ5mT<%UT@Uʮ?LT&0nqBjy=VƲX8Ú2>qwyԧ NP.S)0Bϑσq|!~-;fK wlA&޷bb{^*ܻ&{h_wrI)_umx2ήx^!ލR(I4ZLAPa>#tu_-v;;;OElT-SW[aN#8LmcLa-]y3<@(*P!h?=;pyO>*_)htQEzxy{ 7kI=W:*́%Rjwݼ!jջ:h[ZFL8Rr>K.Q^G!&cw2T^O%k+ 3;i3aq~]5ܸ}d39='{JIZwO:XƼ:ޝv"0Uz6JUt'f1zm ʱX54 "iC-D}Igԩ[^ :Y/!ρmfpM;Lڵt8}jX.DנtzֈocOraGz 56{n<'Lu٠rC9{WhRY rCAgl^RreȢ{_d&4\$55ba_N sRlh|.㩿^^gR#+=bB~Oqjھr)[}"_ K7 JV>?'N [M-ko Qso 8w`ř[2E20'wXS^z[p,)cs*üL9s4ȉSl|LɃ7E['.ģzBnh-٦QCk˥\- [011{WElNW?os}ټNLl$!vyk<w(8k ?E~7\D@g.֡qov_(nt2 pE ^gLrHihME}M`;`wrrr#.̿:oZeڄ5& y?#F]6vnE#Z4a_B[Q!7\]Wh;I@ZsTg)5n,a`aN$j$7 lEVf=v!-Xl:2AԊǶBH%_gvFxŤdqjZw|G&Hm=\GGy=g@y2,mS`40(T3oLlh͗zTbx jLťVhJC`8)GxW8U\`v.=q97?>r"W9*ÄPlN秊v|܏]-jY=& Zh,KTT0'U;F@g{EREcV*nY9-Cƭp|~E\hJQ5S5PK_3|_ٸmPu4zm;j1(O X4!~*,YjjjEi3?vrrR 0I-_ =0WwQl(xϤ*Z@ Dm/h,@ jbi}Ĕ%]YM'%MF&y Qm@B ڀ6ɗ 4@ Dm/A @2jl[ԲR8~8f3hЌ@  r@L@ fNx@'< 4@ fX!*;;.@ h :c@ DX!hfeaR HJ`XluҲG/(92ty Q0jl԰m^3@ /'7176uw0!=92|lJÀ!Uc'`z}|tjZU@ \{9vI;:T.~o>}G+%bXNu'[Z4n[m]'ʾ[En,C@ u/rN?\",,^wc)beŧ{`aGY|FknK*&?rŋo3K-oFvxBFu/?_"ag%ݽ%7WL4jdiwu +Pd|>_|j^>\z͞URTߩ%nέhl>K|H}@ Qr.]dm6=w @&4=wkWRHϜ}MIʕ<9WU\@>l?"7(= lF 0ƅWV]:4`S0qx|:ge?2ߤzVONZl c譃-{FnLvYN73#i>i+Kλmý?4dD͠!0(^NJղx\*6B#-4߰וBz@8a~-I]n67r DM| f]6}x?]O6W>gbBQcb β*5pͻE&JdrNƷ1"u' Oz1p ޸]h]f9M#qQS:l&3h@ #ɍJ =!C.֤}tR.K:W"S&f@cҦYѸFboHyr\۩Qj9$z7?>jk{GZb敶7f^5NfMՒc}I{7\*QnX.F.=h]f9O8Q%#6^?z5ңi-Q9dҌow~r`jέUw?G4(t3S S.Ώxzj6A Q ۞H3~Ӧ-9hq|yATN'Ij夊43}3, Z̼7-C6yr-3ֵαn}șr#ZwisYji{\G9kS w̃r2, XRZ΃R9F-=溚@Ӥ6nzy]9Ɩ"&: jEI-PܝB K sٶѩ[riŲmSo~?Z'zzE2bⲳ'԰SYtZ\­鳸YZM>O\7]Ϗ=Lڦ-|\F-- P~:Xޘrm\ڛq&'8wB9f΢5fy had V /@T&U]jsAꂶ6Y6flԥܞm66!sIϰe*/y?_c"B7;" ñWKQC.b_2&Hd_˵qRשv6`M@y9p-ָs[Jƶ9<黕=3>bbF{c Ͳ|W(`MlBcX=Y<׳`ؔ ] 2|߰P4vw6/?'qx7o6֤,orK&E-QR[Q4>QHM[I@ Jƛۭ kE @"b&3[ճ\ԭwpn,hk+ORS[coB?jD`MԢԯwHn"~疧4Y0#q x7d)<6'[6ܙ^zV}@p0 b1҄xͫ lbZ>guRO :y9 HFg}J,YtKd<pE(r C¥_\z;at *.ۢ$'dG; U.^9z*YQ-Sl4c}]].IeIZt5~np7?kL՘9žWN>`bltH>)zVPG!Ԡ[I@ Mr$?E?Ccո>Hΰx!T {:F}W[ "ڰOB-~} ,5*p‰ȓ>bA7 ,O2 e\Miɝ2MR:=A^0(t3/-!h Ôc X"1A1* #\GK>@XظPbyC+_ܶV柳%.leo FL={A*17]PX$sqKVqj  OXJὕfk&& JU!*9>]x?Irsv3__~^L O ُ}zaXn[#Eo$ r7ߥwW\jd&Է T@0 wwϻ2(r_ :'56o>峃^|:y* 4nA)ݱڰaCDDL^bEHHd8>MA ;qvao/*"5ɶf:&h(Wmʧ:% `S߼=s4ߑ{qٓ-T `eW^G)G#w̺P?6w G ͹7|%-aŤ9cE=%,Qוqk%!Q24ã%0uQW0Zvd}-x%,M;NUx9GgAAU)'A}*a觴,ª@Aԧu:x4yX?f{Z+ hwBrAƂ  HTt1vW=[_z^*3(ʢM6 Gޱ ~BJfڢvXJGwAڧR{Ҙ ]+ںC@AG999Zk*Ca !@i u'+Gg &$Oݸ'%0(>Qa4B?Nt-X2¶h8IG]ӻg/%]`0mebGVeQod&c#(:i?g\_˦ HemF=.h7aR\@3#ڗ rc:,kFsmga-wN5jg*lLIII,RVN=*F@o\RڲԍfB*'{d%82՛8:)n_4݆+PiF= .#ž-{T݊%/4 ˷]uY)U75(Ղ1jQ5;cJbtbl s(\ &)tRO> @(- O7 5J.Qo%4RSV028݁ˉw\X}5b>g42dALqrr:uꔇsM ,'lNA,*~"ւ_rfڤXf&1 :cjaf%$\%d^c*mtK*x~X;t89T6;  Hmg^^^IIInnn%&izѬФ ^_0UYrG]NTsO~v/&ѭSw#z?`]vbn|a, `S5tmw9 bѧ#O=0 )݂vԃ7h '}ȦQBq4{0aLod[' jӡwrSQk{iYO7cu;g5ਖVm)j@CirkBVՊ({DkM5 5Cϗan]Q!1ȍ rf#'sUkb0\{/7g=]сUgªW*S忩œvV0 >1^3^>O=MәaTZ`Hiq_Z.u[m?dWBo;̋MgծKѫӬ_F_>'E(nf;yM0g$0~^e;oCd88F[j?c7a,ˎdz# pox;Z,p@G&\iն>ޠîuLsJ1H,*22 Zfq{?qIdأw2ԫ=p7%;񲦡ZP+hZLb^4>}NwJ(CJ+i{{)x}/:捥;N[X;*">EP|3H;vFNǧ >>MV0m63&m+mIb. -gB9YdfYxZgq#|dcN) >U5qkmVOzǶ{79Mi.2 [_pbãmF鈗5#yT &jI__s2HP$GZ XqN4?ݠ`||6/<޷]'KܹcAP_q"\w$ ^v ",5@A]t4HrnNΥ[f$eϼ2,pgMt]LEPxl>)kW *'^>|^qdH8rOߒXܧG Hy;|;fMTwCfY#c@sX+fC<cNVw>,>,ʺf^𠞝E%_:q PݖHzgpD2Y@}S %os|~d*_M 5Br"e;94_Z+uE6k.XG_7aj֞n7 Qr@@@-[^R_ڶl,*nGzy~6.Yvz+ ߌ_$w^dd Xٕ{vQ-HlEg-` ZʋW)Զ`MߵXPZz۾eOhaCfctL1{`֝4 qIҘI.ʀ%7k1WP̦JRr9X.]L q'lљ e4Gl GJi)>lWIP::kp5K υDG3 ę8NjB7.'qaQ}9#xĖZˤ[Ro.ѝn?>RRY49P'cbdVGf|L[C)G/ehO0R`J5$R=Z˼YP̩*Aj+"heQ٩?׸xNFLrQlGrp%;kC{}hTP̦ԩSΝי ii*8\ŰOj-W ,X9ݒJ ?.a2Æ:rzemj,'lNA, f(0ooAe:oN)sZcΗn~rbbeӟrNPlCOw s3dGl׳s/u1JA V 촲 ]?NewlL6H Pm$UݵJJJJLLtssӗUlZ+OOG3wЌ:DYu.?݊zh:uwO1#ȪXIv-mGڏإ蘺T7x|U޹!{ ٭حs\{ |Suj/bحwrIqO \{yĶO\l]Z7dY^fۂ Hzjf"Yͅ@˧9p%)[޽ fz\gu'2'INO<4u7|PMCyso ;+T 9~IPj~OeCSF΃YlE̫;Fϋwi_fAX3#f=뫬At󮋷i 2c*a&)ELBJI p{Whzzlp##8jKg+R"bIf4j1T)P29:ݔ1wU?G4V :-j,jUߞNZ"q0:N9d^%Ȑ4,a^Χ**@U2UJj99Ȁu>tZՌYHժVB(o3Tȿ3p%Ov=2'Xyjot!9R*K~(m\Z[ȪkPά#8eP7(b *-?}ت'U*g^֒VA0᫥w >$CA;Th HII1ʱt\Iut:o\2 aW2dyg|p?Eο@ZOHֲ Wgޖl@RHPo[PzOM2VA2h֏AA4WWW**buEg2s?icl Χ(#9 P`Π_rv?{#9*BLB'5)TYM[XadikA ;jH[ՂaZCwDNi899:ujΝd2y…:r"- _<3 Tpi6y;VP ]0ik ߎdx ,'lŘ-T @)Ȍb0 DY2yXѲgUmRSQfp*PQƌ*]uSBکP-Z#`1V^^^IIInnnҰӖM]+pv[|掺!3I~ksט(ьQrȅ 1,@dCd2ApI\#N8{؝qk2BTgx{A-6Eƻz*퓡U5qѫBUJ|r⠈ ½M Q-hD\%*xXD'qb`ۛY3 ѬzU™_ʹO&Gldl& i! H o4s]o:/>ڜэٚ5Ҽ/E T/ zM)kRf?`7f^i]mS=d+4dA2WLЭIJ@΀boӢ÷)J.|h*TJEg2b իCa t4nqJ)g>Qٙ߯//`AT3՞;=F: <"t$hne}! uZҽtCO? Z%U&}aÆ2bŊÉ%JI\*eDT]y/ɶf:&h(^dO3tD'q4 39V$hp\#XA4 ʟ4ղKpA1#[_sאY$hySXmLцE9?Ln@#C-eV͛7,YZ ߿g@D^X20 UJ#f~&AnL]e-hNr@_ˣ 6ڍ'`o$Ь}1/AAJa}'^4`񕄦#Ίh,har82̑I!d@fRu[ǤLk)))e |w`4(Xr[s kcn{7a-B! .#ōo\KNF蔛ϻkqD>`B(ϐ!?Gљg\5qֿMb̨a>~ރf !=_:g؏s#N1FWWGo=wϕq骕:Qd>iwn{+5^T}Z˿A$YRLrdlllllۼ`ˡF4q\5<6mz_?\94v؀>~#c1 38[Utw/PJ FTN٥EjX,3{ :f{Z y4G@U&Hq̃?b|F},$1!>ׄzykˡ|WnUb~K3;jQ'Y0Z$ܿn`}Ծ_Cꌚ;>ʀ'R:-M^W kf|5t汧S.^xe8?A!$~œ _4ЪҦ/iZ9NNNN^sV( P /?L:tO~c|DI->W? +3+B2ΆRplH}RsgP?8_tTǮS*69MAcwתdհ"G9bgB&O:lκC #'w)>+Lq"_{NuZY Q5Qd՟4%ߪ2Cٹxŕ6};R=/XׇYcۛy0]ma \'8{g eaf+H &aOأ^%W.ª?m;Gm9~~ ,u{TTz}!J釉ajߝ>[!6Z?V?MSdҬ@(ƺktrKGugi#EN;G_ݤMaoHl ߂E>H`IsfB82[F_kC[º RM&* dY @(tTNNذŧFO_>\UN0 ym_丙$ Z3keJ)>SqԥFaolʮM}mS, ǡRz=ClLQ?Յex52 e 2kT)QlB'<>f/vLke$}W%Ƌ U/пljšaͤ\ff׌rݸ޻έr7N[NGqKN[3wP} y˴eT)>ᴥFmTDznp=jJe0(8 _T*sgyz1E\)14si3f(D)6lf㶬a`[}h0y=2i=!W1gA ߰pO-d%Tg%(,mN#Ǣ+Ts8ÅV[:aÆm`G<;ܖ:Ǟ ^ ظж笓i\{t|_;Wϊ1YC|0d\vOQ y4O2r~6lfsC 5|=P V$S'߼S֣+z_W.xŕ6J>暬guZZr+4 ;N3 ߔMΝ uXFjTDp綾+q_o^tǙ%qSHz{ŇV:}v$dLq~. e1|!r.i/BTFY deJкք.clwg<:PXl#8|'"WqMfPubGc[5>u,vmxYGO8V#2e8l<01!>9ϱ~+njHMs'cFKmX!U/ @kMe1jD^#w&sD>њ:rm Qhրf 8Ql?$$ǡo<\0$~JXwY5$>ȐڧʀZ9HՋ-M} AFqT  5oꗃ  5o  T5:@AlƟ(TN;Wۖr\+xdFG  j  R;V  zP   wBLC@At ޱBAjݱBAvBAAj'AAvBAAj'AAvnDϣ2nY-f{"Tİ\ m9N +1,}vJ}ęʍam,ٹTޑ72H|nBDDY}.Td+CAAО/'(S&C%'-:$c6&YXk%r\n >qhǿ$ԹHL ޿T9.ǭʖgav˧6B4w84I v+"'bF2~#\ipH4~M>E qHr]w a2\H02͚Py=3qeo8:*(=̯?ϒKer \7RCܱ)Jm؆q/n%SӺԣs-ڞ7m fE^ͷRܖ]f  }<$ہ{Gq<Ὓ?MXV8] Z1[SE>Ȝ\Hɑ8Nڋc\?4tޟ'c_>GS.SosI۷u_nὑ(zΡ>9P^9bLZ.Nߓ6G]̻2?=  Bsf)(Z1ۅف% ۫OLgeZ,f_]dգ:O_bc2Ԓ(|C"aֈǝ^|lt#s@Ax[0X^IPgHخMJDZ,fKߍiL'mN~Tkރʏ> K3>k3^7$*i}7SҺkm/f2(PB ~r_HMX  /'}G>[pU q|AoIJU%\1yXBT1.2|ɦ lŨW= ( b0oU$˖-L/\_xA1!w[/v*/LhG}bh!f]F_KG-W`6$]BokN &^ŒLPj]ޫ"s_LKV{ AAnJbR1ݞvq%͖v"}ĺ@= .5#w=I_?exN-Zc^{R]q HAA?$vp"q A5NY~};y3I5ߏjX5 rm2_|~[xU;΍,4ˤgd5W. :V U̢:oK/?н{#9rʜ>m(Q-;^k .wYuqh߻@I}ܪ-쨚[3v(R.r{_=#ڝכnfu nԂa\qj]2ZEEJc<7.zO@$-]]3V7O1꬚2%)Z2Y 50V4deweb{?6`+ǿNԨ}넩.?`mص"Uk|*%ޭ 202l]@Vawyϕx|g&moQ)UzMڭlMO/VC֕$iZ^bXowDSujث"#F G)bvG[w# wޙn,o6[UFJٟA-* !L ]}/"R1t[b=]B1< 2>׬Vr[>[ko_QA* R܆Ŝ?k[i&@JPМ ]zTڶ+t -6!c?;~L/jHNOM8o>bcf27q~FYz!ӼUnq)i`߹DoXS *D-d7v'z@rJޞfo%۲[)rB"+1YuDoʐ܏4@&ֈ:O;Wv FA4[hY|S\bvo>Ed$YFЍ rKӀ i"P ]jd$lrRDƔ j RŜhݥ]fTLԬ~`HZ۷}ع1Ky|U0/KILS5ը]餴'3.:**ik8N ' lJEz0r 2eLR};VT12E딄[Cr3r8iHBI|쩙>͔s}\q̿z1Ŗ%W6Y7hb8aw8|ʇ L'}{+$.AoӾ20Zr[2dֶ)W T2Aٮbd'!Eo>|Е3NipH^,,Ѯ[$L4q/ j.x9rRΥrf->xy]{7ɇq͌8rܲ ao.=LerCW%'5aOr^$/~2b>2{ok71.Jƾ?XgӺW#WmV=YI%Ժ)~yVot']uHU [ .5\W.|#G==]]^j;\s}+vTx.!dֶ?ӻvPUW^{l)o YJU\. ȔNmU=7/kF+VX!j1jq~NB!AX ExB&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!l*!B*Аv !rPKKKv5/CCCij^B!ls9v?$w8h|/ P@ҎI_aaaS W344u9|x !B B&rp˓LQno| L&Ӵ)L.J7B1n2LCCCCCC69e[O]GQWok3Չld;9?aad2LHs"ZY˦ŲYN?3 $5T ҦyOvE<>imX,V[Ŷ,˄rW˩YX,g&4^[YUe*h$i#]'qv~EU?2rӢдY,kLiJdM(BE*IYN&fٯ P^ĭ˱nm-@j-y|v,ѨvOPh:{r_gw~q[TV~e+u&DP="8`tb+f^|^%2AhE,y=qc[PڞݦU^4D)~4DP U>K"\z%z>7~Bɑ 8;cb}7V;!d\\#vĔl [wo<7'@(/J:43˕9&um6W~ܻkJW3^{5M@15%|}q|+/nqz/(8o|Z6Y|>ݴi9!#g9Jtާ5oEhE\Py[nk;kGqzpQA =f!$]x $ʊa& fYN:I Z=yh;GfhϺj@[ceᬾC64-$6msY߈p)F3MU hs9lz^467Wv7nW/y:~BtC;9@e}d7j!dV9E2L/HYqk~i9`mXLgD;>tJdYg-O8w߇dNҮ];-%}Mt SCWBI"ě@)twA枡ɍ<3g)onP.>uH%ϏK@W'N4.տQGx ]x@Bj;M9^@=} RVoo־DKj>,m$O>9.S>[эgR [^OBzLş&^ЦJ*W |V߉]WQju\jAG6Qo@4-?okf@<<;4wgRSXnVzs!GH`2mP{ =J( FDbjm0YpP)OYt"*BGJ>^} %O&ꢂBաlܸQ1 髬lo߾l2Izr˓f-KHwЉ6(&|t@ųnfw7s3;%Oo|! >We\x$q%[o}e?Nx=Zwon=vwSUwNWh}_Uts5P٫#{f͘yj78}{g:6Ob̩;):͘flqD a9W"S!Wo:=`.z3q-W/&:;N'^M0@h،:r3pm/<|pԩDVfe QG_e#; 4Q/TY/VeH޽{===Bu,wT. *yE],2^ 5~['B^B)x}Ѧϟ—!#AGA{H7ۆ+CijFAюV6Rh B7x + X!*\ K;;ZQXi[inBUs9 Hx.VW#$DBBCe77r?aB!لUB!dV9E( 5?Wdn144qpd~Q ޞopWeh͌{œ/'BH8<>H ӴW1 =OӮ_k<̻ﭽr$aTg_ i?֥[5V)ku0{W?c`GDW M2:B6N0EuHتDڹǔ_8"ZwL6w3|2g~ dT2U:424+Xxs.?=)PM[}t^[ս;||5HL&RW>oyrvR /?J~ŷ}]cgP2ΟCW^;@C;:ޢ堟"ΨnZv$%啕gX80e[ ;9Vq=wqG{BW38B->}%D5yOhV$+wd$ #)i`Pj@OyqV?eZa3;O?}D_^Ǵ.NdqgVȑw55r=ɓ KwT|n)8{NBY+:ʡ?LiCͨv:q9Z/.8PKUh>ewvXǥgV~e2F.v$]%sO9-ZVGّ^K 4 䝯ygy97֚xOi0~eNoXxUNGv lC_W.=dy:T dCNI.+BhwW.~ݷ3C <Χ,g'} jjFf 1{?ee<E#2UةNr(69$q)h$Ю_P$ X"?堟-.f wamvsvnAdOR R/p>s-_ 4htA)?]vg}Pԛm}5Kg- <#vKD%QBӬ.>U{Xg T$Go yaw X줪hZvh5yȽ~#$\g%zd؅dJ[-z8SDXOMQDM [tk_&i$*+/ 5ƪQl[C.[Ep&%=f[*bCso".5(TSjm\>5*C 7zR }g6=<*4,'6r^71CfKSKzj^ۑ{>XWB?|IOO8c &m#[oWZ>>Jܹ%NciK0ibjV= Hsl8s?l؉?.}+<|+/" @;%r'ӴϯrvB$;PE_"NԈ9AʹtJ ::42cB(c7H Pdn<|!a6PL֨=Vj-qIj9\5gZsI<0_}gU)T.C7,nyDoXg=%PUSt&Pwm[v$ڗFrxzL.E:lq}EeК>c em-=9>tFŋ63wnO.(-"-ڰm_nrߐTDU`wtw\ >-kG܌{ڨWge=7Pgw'b;Ǩ/wO/g~#3X“+/|7tڍ#%v1X:u&5Odn1 ٷ· Jo\?򜇻gSWRPcDu{?Iٷʋ>ڹlOY!A?]|UoEyֱ. a3<@#3G_\;' ^bMV}<^[8%$'wݛöÃ[*S=k.;Î)P@ehk_,z44'87FPHW/ߧ`a_r&Wy'YYg|n=Yߜ7rtsК=a\gZk!$b~6*ǒyE(qڔ[ǯ,;Qp`8TgC M{3iӡ>2M{ 󔨽{ukњyW$K!Ǻ4v#f+5& x5j˼Xz6d܃v5">?`]hS~'rx !ԼyKQR c|Wto77:k.:9_?M%S|C#S~'P)w{"5G{$t$/r'cV8v- K2տh6;؆J.)O#Y '*kAAet(7N)圥cڕ惫пEL}2i: ؚsz2oNA*?g2.4p18u\Ql[ @a=\'KRNYb\Am?kR@ϒ!zؚ:aA:.lpn}i_n !>PЎވ|fTug҈H։p7 {xJFϒGX,v-FFn8FǞ8#~= eEmfqx5OZ{ϕq&wgX-q/ٯi*L+26ly˖4%ӫ+$.[QĶt~:o(ύ;ګtϻq"//cߚ y_|375V9;1V-$v>yGB- V9Q:)1+ &JLL\kR;$yJfZ vt! ]Vʤ}ߟ n8T Ϋ̬>A f5P5x9O}^2Yt6-*޽M6"9Ynyf7OEW]Jf+ȍKxEs٥ F jl\ -gω4P:K}{e_[;xe5iii!P;{mxql%!S]}h?LiCͨzv:q9Z/.0rPUjF4~n2ͻj;|ҳO*ܲ_jhy.M֒9L&d.ZVGK8~kR)@"QDQ3(|=eWll oKekR%Fe@&Kj DKxDUfQ۰fK$S^mZvh5yȽ~ظI+rg|Yl.A!va.ҖFaNh>HU^)TpWW`wٝ ꭹFAcu}ͶU9^,7x.]eG MSNȍRr^UrvRU}Ъm UE$*s*B AͨcШQ"?Pz'uR7~g3+#K+yrj#uS ^f91jFKy 2M J/g箐t쎨OԌ9پ(4ggo)}0miSG$9fw6Yi6[o}|}%QE$x$^u\h>K \(ȄU]S3hozx_'oIsK!x=''4ҿis ~M H0ibjV.qfʄo@&R;.B?AH^z0G˝ˊczfYBUIKEg'ͩ}ӥZӻ^|4UN0V4Ǿ whqiG@H\BHrS?X< Is煇{'숐|uL7kh[ v#j+s}n5# k!$MUqȨƤ_Ȑfdy$r#NxhwlC<>Om~) c@KOOO^x׼ut+ۙ@骯`Lk=TK*[Wk|~/m1WKKe5=(w8Cעm]I/.dQ];le<\Y強>9%|ZWΧ( M6o&0ESUQw!Fal`UJGQ;bp(KoU 4*,, dAXjV%N-!h"I֫{NID_Ivr$um{詾Y7aWM L*^;լrЬv=J8,+}ŸnmNǒOLWMIޟebWu}񋊇.h-^A)+-?w7}޾[^)⎢1AJ8fORXүwR!)*!$M+:]Ľi%T9t($gr}J n. $:;^!U@AIwkc_euץo'wN~rXGtU*4b|"pbQ}`Hrj} ԧ0Z,xU;nQ32PMYlU %z̫ttG:L@gGV;C Uγ=ࣤvt# %m(fd6 x|O3k:huȨuIV%E\qjZ,_2My~)R>ˆ&~[:L1Ѹ?#IF=gRWϵSO6d; AI]B.q0A'Z3 Ps*!$MFr4ǀJ*̭BU ~߸3H#t?\ITR銿7})h+k<ڳM/] >~\^`SϪjGU}eoc2}]S/Q4*HIFa?ig#РTyּQ@g*!$ez]:p8r;ڣ"v%*oYиDUX+]ktc}rc'ǧH];ٸӎےmV=YI%|OC^d|rOه*Kda5Z_hO?{r>s4^4&4Lw)/%Ť־uQV R jgP\*Egf !Z&+ A!l*!B B&\}A!>FX Ex !B B&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!l*!B B&rB!$A!lJ;$k444Bg(((v5ҤByJ;WB!$\j.4o41 /0Z@ v H j( b344u9+V!MX BH6ad}-\1_h=o6& }f?>I)7ˌSs:`M?Zr$M^aW ]'Xq΂As#2C\1>_GуK.Zr#QV3ax<7;ϢxuLJF92CӾf~T+gUŷi1 )(ُ?!ހ {\Q@ }tGuSU(#d#1[1{;b9ƭv2g/}@wT_P}EkQ&4 jZՈR>2~2?/jed&BTJ׌ԑv'5X/qh[Vjg$2! 4&Zq  ڵ#[XTpI2>t5:($$!y[YyO"S=tkX2?|CgsaNDq)DfԉƜ`)bMhzު+D x3#'/a(X ]x@Bj;M^}ߋp~dОz{'ZRӚC?'@AszuQWWUrnYN쏌+HKF|%#7J;$}M5ۗ-[TgdY2ݿ7t"$ʠ}/;]$P}lg.yFx[g#|O8s^T3B$/[gnZ$|cfw,&3C޽{===UPoDmNq;Q".`~/tp[iZN*?:B-D6]'~h/PV !Z2\}ov WԌ^mW P W@\2\TcFIFJP XrBUjp]B!dV9ɔi`$B V9!MX BH6a- ZNp6~Y%< Bt(Ja?uajقEzmBs>4Q Jeot6r܆Odn144qpd~Q,j7@Z .4kM8=,[gQAuUYЫ_h iLtuDuP38knl>%[-C|g.Ln햪ثr}lc&W( 0evC,z44XMo'\Tlo<#qŵ#_}%t.ɬ!Լ8e/L@Zdx1;2V[(4=_V*, zFZ8 .*ۜd0>y_/]YC*5#5#{`gcw~>9 8Д,fn8FǞ8#~q[:|Jj{M`px: 6;^9q9G{Yh Xտh6jv+~c$=.\ ;y*DZj!J4 9F ]gokk6[8~BR2[!oFͦiD%\hl˩Y4T |#>~L%jopj-AҤI,Z'Dh>ewvXǥgۊ,t+I]%s,ZVGWkh=:E*B7'b_xlJQi"u!{I E#2Wc'SU6īʪ'qSQ)jFZ2Q .&kDYRĿ#퍝5Z,j܂bۮpwfg?W)ɏD8K Um;BG+c҉n_ 42<6 Nw񯌟.>]xs޶QEw X줪U6QF]_ UvX5*C 7zR 59p=?yë|N U]S3hojx_' 6jڡn_ѴD$*f14-(+W(.vbX%AbJ QTwL~f 8-%===[+mJ0}RK \6j6EHK&*Ee9پ(4gg@;bAus9pPw5iǫa 8Њs+ fZsI<0FB*=D*gGwGWu_ڼwЄp2eCiX4Q q0QT<ʾĘ]vLi#,mJiI&~65#⻇ IŊ>uyu:T}4ey!=+% lw@m7x!'XQ'cL(#ECU*!ԌhJ65dq\w'l(4=wg_ i?֥y[=b&PHn.* NB!/=$)7韗>I2DZ+S~'P)w{"5 پW"(5%}zt{U' /1cˋ.{$4*ieO yqWQFӤ~ժTFwm :.¨Nc϶~rj 6OuqSeK)K+ܢyy_y&TT8Z<@HãVsiUbc]e>pt&xE(5WKW^۹cnE ^<*44̥x‹̅k}r@p8Cf_ǏGF] *OZ:u!|O./*.qs#*xU!_4S&0i8]<͊d }U` .<p-fu!$]X ǡf`w^effzȫB#2iug7=;>2뷜@q~wknO7 Q1ϽS)z5/hga4GgU_ogVr蔈ϕ%&&vV*ϽyckR2[!oF%"~#U5zsPkD (t2ίIv8 X!<,>Kd2 ܯ:<_FgWt#nɺ%ԣW秫s9.ZiRɵ-=qqm_24~nڻu?]a ʹTSY:s ?Fd>u4@Ci(Q};8ECHAHR/p>*h)RA#C۱ﶤ{ ӵWtQ840[O‡**r(4t+q< pF> +/fJ#1QD=s\,?yۣ<?^Y1a+Jtg;4Y(hU9 U]OKڏOBR-"2ȩM-01sbՌg[]p_f,zZ?YgÑIN_R1bUSHo'DcǠQFE~(2FO$ڻ۹KIQc~\o1waaB-&$?#?+J,%Df1cc3S½n{VK>ys^>'zu{ jh/u{mVj{@;ݞeG4ֶҍjaӁ="Gw*;ucw~#rY""2~ez_8Ϧ򅫃TjrwNJDS|}7 ܟ&pqMָ +?Rq܎MȦ|RI yvvM Pvv/^ߴo?qh/o[ Us_ёU"17SU>Tj4#"???y ۉaUr3ξ+7Y@8i/ T90jXk.0|u^tW+:Q֩ bӷBV;u,}Z`Ohк?qC|Q=8^UվU]7-us ^>=>SGa&v:BU؅UdR̹㸵U^GU@#V/- _ ȈH%}oЭ~܁N|tiMU/&DqyGW:V]11<_|`}Ӂ}s¢*0h 30233۴'CDkZ9rF& ?z9#fv3^؝{`1(+aXo97aЋߚMTj3zZWi_;$.H~4i=\CDwY-=$1=HQ*V[{""xuW|)e5D3BPѣ>v"Ruw;DDCD<#"RVy1F툨IiL۵g 7 LGD<~$dyqžsVhvEeE\oi+Xjڳ{/˛I!mױFKr@ODhUz*?ɜ*_)6MDs#c$D"OJM9beC*o&4GD׋eݺED&=3–_QZrnD-[^+gZ 1=3/Pruƫ=*Ujj"ŐNbM|j@$ˍtrr*/~rUJ9?{ss٢X r@?#yr"vYNСC}7i5l̛Őo[ǖNbx4!V[*#T&l:пUq{BI,vF'"ҨWwr_t嬃9=SLWLxLoDn;KDQƯLo3|GF-[ wn`b㪭YvDAim +vE?-KPU7H2]B݈H||֭MGnF]"Ø<+~ӵZ#@u3ykRfgz88~3Ƒ+tѦ.f۶ 95K~65bXAW4rwNx9cRvw70j4C7q_Uɮ o|bms ӑs/.|=џKivBEDNKKs\qx`ɻ*Bn7hQOJ URCѼ aCږ~KO%qNZN [֯BKv۞)ЮZTy5IߓZW^t퉱ؠPADeDv9ƺCe_Lh8 8๓g:+T9@Uޫk!<@y{sq߿CD}VU+_Ƽx^40B^ǢJa͖"e?q-^raa{(#ycY7J]3f(/m1{PDD&ys?5ûo}䫉s|&yuF.9Cw!7bnАD˒,[3iiZhk\o<83ΩWV_|XI4rrwwc'aIHLќc⸵Cf[ò 1ޛ6s{y?JvOIoA,Z`&TOÖde5wlHSbv}[Ahxx̼h;[7mAo zĢ#ywg>fŖ_;%l-Nr&"nMOp-!}"2mFD#kGDc|ډԠsfVD[צloy(mv&x'KA ߤ>'t?qG_+ /@`Qp-dJ"eX,d´֭[7oRG0<%oT@[&#u#SMzU4PVgч2.aڿ#oEDu0Q>D"9sfKZ';߲ "Ye hZUʔUL\7T9pSfDT "3h=:'e5De田{V놀 &Y3ʵںvzwHV0/ZAd ءʁ{^,W,?TxQ3 e#<3hJDޓf͚ͭKOfs'606'$s6.Ne]&._||+S,],sT*&{w7+jΩ_,<"=Y;B 7T@ jHOkr@?*/ET}P2C>G.Cff)>F---Sx!@@ U'>x?N_rɸOu*:hB+N͈s].<}cc Xeu(ooYXGm@gY"(}5[kW;J(Ж3Hܟ7gUъ;L=x CwHuTۉog:5DGP*Ei)ؖ[+xz>-OMtƌ >];HФ h4;1Wj { @ʁUJ<ğ]=?<|kg!3=<ǎ[X!ŒD? ;~:Kytp3gBV詳ShX,^s屎 NCzLyW뾉d;//F]|,\>~BF):۶-ztXMFFL(Z1legjT9P g-L bǯo8?!οmu[+Yt ƆnoW=Ϙ{H[^Q],]Qx"ꯂi>"""f"R8ne"|lڌDDl7yߊ rD{;;VPL"zXPDNDJۍ[%'K7 KG[  HYn>mv.!iTOlyJV-涯vn&#"{p γdK~7ex qq=ںUeI2u%{P\ ZZzU"Y?E.&O $?[aD472f`S!ג8R`P/شQ*كѨAi Ԭ\֘2<[A[p7[u/G2ym.^^[&ڃZz#VPMZ:rTw"2:ΨDDUN^WŐo[^G9Mq\"24'gju)h`l[&qWH}x+0{K!qxvWxLID*imY 6^&:?=<<)ڂu|\Xt!"zKF'}60|>FWkk݌vױU{,}Az_qy%,1-& th_;$.H4ZrqH$(b~+̭=u<,=5?gNlacp?0Gw]%qg-)a OPo%dFxXygzzzGcFQx"ꯂi>"""f"Rk'1lݹVIghM@H;G2kD|ӎ Ww/i1o~>R2!+r@ ?5^ýR.=5 *X,3޴R)CJPcԎEvY{nڳ{%Ab9=5,Y7#Y.÷V^[r H/kV򳟚{1NuaD472F"H$ԔC)6U͆6Ɩ-"%xt7k%ddNNNW%Hq^#2c"2zr_fR*G.FC:ϖsf7i)I#yr"vYNСC}7i5l̛Őo[^y4!V[*#Tcw~#rY""2~ez+lvD3.>Ԉam^\(9/]ֆoGDJٕ*#Z8<0]QIyCf4c آێ> stream xڍQn0+HcKAF qB _c7zv=ڙp KBppi!HKa(܏!Ixc]6C*U`ֈȜ$F5N_'qK!5! %i4Zh&NN{`zԓ7,*nc5XѮE(I*K"?EnAN8% Y%'(P1K> stream x흉սq&6,lɍ;mwuQW̠x bY)̂ePM0qj&\)B+(j׼yWoN>{C>y2-]bccccclllm['Mzh.͉Υy;Mr Fs虿wϽ_tIWܽ)E5xK%5Q;+=./7晖i.ǿS66`Ɩts<<|`܍ S66q8+kMrk,eIZe”w]m1q+ـ5fkض=.Gl>C/_=" 3H~|/G'++ټWߙ(J 0',8ξv?.g 4O|m7 F挸yb|_~5%|-KM/JOW×үp^+R?(rgtv_|?wڕHWݺ=':rvȭNyـ5[#kA.q6O|TX{GGOnÖ^:?t8S6;?f;~wt_0{F޾W,"q,yf0E_.sл"8eLq?Lq9h?S.`}sE<9Fg]?¹dO!sO.Qo2痿Nyـ5֩\tϕ wmS^?UxxtgO3ZdX~Wω\ؘu=/wSYz|v7r2ҵYw{~9&99v!tedg3E9tʚނk6ZZG {+m"nbN`ۉCD0'*e9"xs #g2S %h` ]5q b99銻QY}oE 2U(a-rmҒ6 玥a n>:-ldKDQdc٣g؛r}⣕぀5FX=ozw9;YO޳g[|akppJr~6~6W~->Zy<Xk܉#}`4/Ob;p"PPϽzu:{J8`-H'}M~ʫ_>%.`mx 5BNwyKcWbQC<{{(o{xC5oKV"E2l$t6\/BX#/Z0eklw_lmG޺j݆G?4L.߸i7}^'o#?(G3 SN_""kx ` r x搟߿d?[?#/X+Fv o[Lfæ~j 1[;`L_FV~HFk`FkBX; kB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!BU/馛nM:o߾/.nj#.\#/0ap7YfE"c ۶m;sEVK )~=9#.0/^}?7\!T-gSO^zPX?ᄏ_|+G/~(^xE"{:Ǐ/E8Z$$_`?B {aX{.]* 3bĈן~"8c|ͧzJ;po}[+;vxW9:k׮GΜ9S6m:SĹF:O/|ȑ Xb9q2T|XGkOeN6W +n?^<~ _"3B?c~G k7yO$Λ \_q??4?A_Ż"5gܸqbOOO8V$?r#P% :#>|(I>'Of=z{!w}o͛9㎻kſ'|/8+xq}۶m~~4'Ηx8ŏx!H-aÆ_O… [^P /"Ş~[*c&d;y!kgxq]ws=ŻSL{}'V$?1NG(GX?J~Qyƌbĉw?K{>,a eY]B񪫮{͉e #^xA_[?<çj͚5_8F<~O{X^s5O7`X{"plnHڃ~xq7PamɷXa=` ,Xx~8uTQO㏅yEY?E;Z`pȐ!3'Ηʗ ѧ_,+87bX{"!~]Ap~ћ6m/|Ar:Gk{g/PZ|y߾}C8|"e˖?m۶,E1uĈy>sz%^\CQƍ5jT`uQGy= ,Y~yܗ_~o'-\?!q,T̙#;O9+v:Tz NDkV^ݧON;mbҥK7l>%`pc (|?8=YaMv$fsCD%|"o]UՂgqHV|yg Yg%v~^zI|~Gqq9 Je|Bf}@!?VZE&B! ?ͽۏ'!B!B!B!B!j:66&lvllFBj*`FkFkFX#5BX#5BX#B!B!֘!`!`k|FkFkF_cwFDkQ\ʳFkTÒKGkQ%pg15FkT-L F!B!`!`!`FkFkFX#5BX#5BX#15B!c{Aw!TȚIhj mJֺu^6BD!`-/VQjkk݄ BUHՀkM|MR~sJLMR_;zUbjrJ k|MR55STS#k|MRS&)Xԋ-"5XDL5Iq"q_7 &h`m`gp _+M5~ π#$*a)MiiR4mFXT"kHCQe _7nrf"ԘlKk|ˉS-{+JkLK Xrft%`KN'Q SUkU`MR5u~UFS|6Fc`u>p[/nz| k}3`1u~ /BuWsc\5-uz5>XDL5Y+]om_=ky5ƉM(_[n"ֳƌߊd6i`2[2ҒS˷יdnAd[UdG |m&?':Y'Ħ68eN Mu^))ͅ5emu^."k1`kY׭Yr0ّ-G.f|X3:G(q5ma~u;`mUbZ:5")Y,Zfr25z5f)T=Lb-taS'@%4R[{f'k*9K~ݮ8Z~ݸ:i5nൃ55ֵެL]kS4)e7ф sg _;T;"k05yIg\n )`v`s 4ok` y055K*vn`1uM1} D >5&MKjp`[뼇x k:VڋHXaj`&m5QGX7$Sk|T' Sk`3kLMRܨ,I)c&)Xǎ<`+ia`MR\5$zk5$5nURSkFk|I XcjL 5,k\` 15u`ͪ[XajU35vqc- kzaTa&,%yWQ%2;q R u'YY]3?K]'k"k`3V,YN]fyVKujv_䀼 ©aXk/yU.kT )`mY_mNAEDcOKXidm?QɰXk`2N4*ks? AүH;VؤuKam33ŜRX krfZC̄XѼ=z"V籨=uVX+\Y`NټH\dIU LfMhD9e<}QڰuEm:Ӕю͕#J7B~z.y)=]2>]5uцyv@e5xͣe,M: q"mEHX{F_7C5麾$p9J5.yM/:u׵.QR ^gw=gؽZoڬ勲_(fU uwi<%UF6MK-'B_YY2Lmkkk#`t-`k3Fx2Wt\)l(ƌ̓uj\5þmc`\cVΣIQX{ :3_癔Ѯevi"k؞ HGrX{ E$–]<]XRmnU3DuRw$pa]05Nkb\{1m_ k*X' .o/ X+LI6 us|w:†r2Zf Lub؟ө݇5yF֚zxozQ:aj`;#Kd+*X ۬;5.֞ya5 XTamk X[$(f 5Å^{$U{X[V?Q ke9ke6K6:wȺ;yՄ?t+wk`m^J/mjcThj/Э]ړ{l݃|^#`]05In˺:GL9sֻ̘ qbcatȂaɀ>\Wf՚utEl"3@$z?Wy:%p u-̈́` k]3yUOKe-ڬ"k/:^`d3>mm\uo-QȎe޺a]l㬋4ֶŌA!"֊6pn^;bj"kj̀u6U]=k lڶen l K뤍Ym9YP2D椲6HE6#G0u"kY]mULGk˛Yk` FهfԺ-mڬs2ꈩ5IUItMlbĜg` vALMMhTY7LG_בyY,#k9 1fņ ]K1u#k%pF.Cq:_kU3mRƼuR\,akXkXJfbPo$.uVMkaj%7 ֘auKXl}eۖMH.w)QYYk|4-fi87EueX۬su-Xk|muHSX:Rc(15n/;ݞ]g?=u5׉`mcC|y ȥ X+'IS7mum+mT+ ,D]V5nX RwM/P&H aέr6Bf:jAwW ͙ig2) ֩ Sm36i ͺoƺ/ʘ6ZX5NA%D֞4sika-x1Ú 1 OuPNr6 ?ު[4o5uo,%Nwʒ?ಣc/S>;uWnJ_Ág96kUk`mjKE۶Scjjp| #kf:D+k©.k"n}(6X7.cc#T.U.ֳu۶ cĀ55515n-y窒-]!XXk>`SNXWضP`Z E^'S 6k|5Ԙ 7u;aפXcjG`5ndd]L&ST^p5n$=5n)\Xk*" 1uL-;=kg| 1 u3icj`+O'2/Xc&OL5v%N'XWJ=j"\WcIQ k/u`WtKN/s[ku1N'::p|ݼȚzT`YktRZ5mnjd`kjp,g9bYAWp-Kd"kڬ5v֖W:UU71g#rbppSڣXcj` Sz [XXkC58M``U-n[=r:e@zk9 xk`.V_% bu9M CrRAia/ ]?1XRG OZ2uedMBamY) ua keE_  p#X#Ti& TD:>=.CPoIg̦[ЋuqL|XXwvu[ @Yg uAzҤ׉yKXF FD'`چ0V>~jhĕWC2ݕfiYֵY[vlҶY47sg7ۼneuT']@aE;w&+ hx"_e "ֳ)"ɒH{^tf  ,'vLU<)̝Nmx}Y"*DVWj൱j0ݜ{1_̈́lKShq.ֵu*L+#\nXhϺZ7R&Q=&^l2OKlJv;X-/6V Ih&tin6Ok/n"C3=Hpʃ5t-gs=/KG<˓f>=%+-i&m2(aihC"k!X'J]cNrDĞŴ{%7^={RXWk7kH X[F֖̂cKR'ӏ]P;]D&esNe:پ8i%̈́]]}_|MRScڦ66kùϵ&nŌXk_ԘXqu &)I$52)`MR(`5IDWh#JLMR_kjn/ST`5I5B8_%&:_kju!5Ƭg1PN_#]kF.5֞]QP`jB̓5BX#5B!B!B̓u.+)ʤbӷ?̤ט4)ݐY͕k__R_v֩(o:6}LJy)ʘO5չvsI*ck|aga{ʩw]HJ'*ERn,wQ*=+ԏ.Z_k|-/c'07u$)u}tIJer{MrQ}}5H*u钭/5ʝ5u 5M2ER]?s=Ky%eh6W$%[SXk|u;[fmHݢcRmyvm[n"5{Yk| W_Ag{0s.0{r/*s f5Bj5B!B!B!`!`!`FkFkFXc&tN5X#ǝ5X(frdYK=2_#`Fd\'^z~};y[J?BX#T_WAɿv6X6m&/l 5Bz7~|_>Wf5[{5kUtbϪuN? Zthҷ~'LYGz-) l_ynwcY)BdIl"+FX#TX 2NytʵߵLPo˖]@Ʉ?~>mҒ6]GzXj@c_ι3xF|[6OSO5>wۛ ymſXȩﲅla􅿮/^Vx~zb= 6mʸ8Ҳ쟻 ڤKg>ސ-Oku?ySdSvF"v_߰#ɏ1x_쎄ϱ˖OX#ֽ S~-t_8s{lUmpa?k}o-Z/ε oܴY|wheOyE{ FX#Şz_0omu8h/tWZޗn~TlaStv o3 q2tγ_}kP" ~m? zے&-iFX#ʄM+~sΘԫ7k5F!jkB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B_p7tS~vmSwŅ~1c߅ qBej„ 7g͚ӿ˗Dmr~-Oz¿K/T(xw]|Ћ/xWbgJ_|!.,/'xbNI6n#m{0=K.^p}1bO?tQ ?c|ͧzJ;po}[s8kر]Q>蠃"]V9sLoڴSN5?Q.#G0`ŊS}ѼykӧoٲEpڴiw4hW\q 7aFq|pg΍i`s̋P `E '=P@>yd]ѣG}Wz;kV{'Ǐ?(xq}۶m k~;c nsĉ{%=g.,[^+^_uU{キ9q,_x=Sv֭??Zf_/^ )E 0kB`x?pԩ 9z 8g8w~ڐ!CgN/ k兹B_=z_i`sb^+5|aFae˖?m۶=ExĈ }>sz%^\C7nܨQ"QGuG%Ko3c9F;mڴc=V:t(lN8}X^O>vKnذ!|Jا̋B'͚B9(/q֫Vn BO?ܫm۶q7B!B!B!B!B!acck†igLjk!T[`͝AkFkFk5BX#5BX#5B!B!B!`!`!`kBhp5X#5S7;tp$#`r`CeapQqoǕraxX# 5*ˆtPFaX# 5rm({s35X#5B!B!B!`!`!`kFkFk5BX#Zk兏-}r 5_kRF}XGgJk̎6W0$PcT/Xbd_cv]S-CB iq M*X˿HwMfL`mjpm1; 3:i!Yi,aSfueu`akaw 2ͱ9ֵsNQʢ7le`e!;t nֵ̢f=k~5a@G^+k(̚1`CYRMk`Mfj]2qu5d5V2@͐yЇaL^<kDfʏԵnDD9I;etA)ֺyQr|+ovg4y= d[,5ud]\Udh` s.&dq;;gqǿXk` L"XjU kg֤+}5H"3eFV;Yk`ͭ>$3>0KzXs5ɌV2[soG`M:$4+3fR,AHXkDf +mk`M:X$2c pw`:Gt5IdtDUrn` u`zdXk`M:eXgF̹AXFۯSz ̴C5Xe{E?:͉9-}Kop`M::KMdMf\H'u#k` ڐaڬ 6_` u #kz``fIdXk`[%` t5Idk֤5H"3 XkHf5Xk2KSmk$`]4z Kk$`]_sG^9s%# 4-#|!`P: oXk2v0KxP ԺfNoδONkDfu^*_Ⱥt"`lXmZf)Ed n٦XGa-'RnQ5|$3MuӗXSUkb ]:fGR6̘S6UE`npw ݊(Ok%ԔMDkUZ -όO@%t$::OXuXs u 3l Xgcf},5EZA]\he<ΐy]X~װX=3:#kKToJdfXu XQr,!^Vx]!\m8ͦǮoen2G3 g&Q+BX{^<}tgN㺝C`awiYC|#A:8++'K'iLmp?LIC%]f>{'47OfDتDj䅮\>&xmHOGwL,m䝅u.,k`` #t#Cw ˌ.U3` Zs#auJv(ϵ[`mL?\ťɡn8Xq1`OS~xu5. q_ ڢN[ ke5ZJCdm`^w^浀mul~{%gXHw6Ae|V8BWDW1SȚ6k`]*QsAеVNf_~X[LYflÁy)\Y˟%*H#FX7!>*i6` [kl$;vh݀ZOu+`{ik`MfTS nG)T}[mWo25n uc6.%KX'6ϒ:t"͘Y7 YV鉙ԡ#t` @MH8|^S pꙎ ke.9=q}jr( 5dFs#t` (+Ͱ|IV kHs?Xk`Iʯ ^5S>Zf!d)ۜbCJFN3`$*bu`]_ (kp8G"6 jGdX4.+qC-25G+5755V&Fe Ѵ9M9%2 kklc5z@pͰ֡Vi+k` -"k:ZͺzrJȕtXg̰k Ne_-NS4+?v]r֥S@ S7pVW_-V&hHAk`ݤ5 =rܭ u!v^SuakgL]dٓJc ٴYk`֑jp/v,gaY 큵yg%m^u`fYG׺eFY~pXW 븛Hf21֪Z JiY˯cGimֺ~_zl93O:k` [Tb7pN`]O`OeAd/] :M kruMa]8ZD֦;iLk6| sYLIeYoiM% 3`5v֭L,'lOd8M(6}&5֩-e%u%YN#86E֖#D:s“(#ju`e^>k` =##hbF>k`]#4kt5aV eqI5.. ]Se~lڈS_Z$Hȥ'|m#r%äLJ&zKxkpsGN5Xӱ ZN2bt:XˉY2]VN1;P;|zxQӗuiF2[a"\fYXVy+ Dv4t,ߌm>cz` &&5HU@ d;eYL֭u' D֘:\6]OeY71^zT‹Zrue-LrL; ~O꧆Y'F2gmZsbBm*tU;-=#TkX#ZkFXsg!`!`NWcNL昔|uI ri'ΞNPNavwǝ2xqk׍uqƧK~mNL昔|u/-K:VgO'COɰNavwǝ5xt̓u2ͧ$dƤ䙠R+H+K4dVNPfK`:ri3%u`I>-~F҉M\'MPZN.K'[W&_]tR_.6:ٝrw|MIԦǦ[LNreO,eO..6K:ߢ.:tSw๸;wk;떷Ygi('{{=֟iҭͺ;tg<]r- oA(vttVgLK7qzkv戏 s5jgP!`!`kFkFk5BX#5BX#5BS7g<`X# 5rX# 5݆ ke_{, ⋯}kM/}ĿǏ{v}԰i@l>&Jj/Cʼn":+H6E:!ѣG?^-[ 0mڴ;OO9q^o,^׮]+G);.]*޽ 3b|əg)J۶m s-|_"D>s~g}Oy\sW\dsi?߿ȆHJ__B80tƎ;a„1ctA4#ɗ,w^{=S &p&^zO>d p¡s{qaK+<{1Lj=_e?3ӧO@0H!Rxs=D>"rx!ٳ# ~+7n/DDA }nؼyH9f#|$_ j%;֯_/"Я~D}O՚5kˎ8o裏>餓}Yqp8/ꪫ{on>~;g}fu~"G'Zj_|xW\E7F(/أAqkQ8q8`ƌ|>3rH9GHZ 77($~G֭{/&1HA`>yȐ!Szʕq[A> |ĭ*=zqQя~4k `-2ow}HJy< _GNo߾°~qOO>vHNXty:pYgI8SEIO"'R/BAn~7|ɓ}jr!cǎH㇏{Aݫ|׿(+<BU}{"4OyXw}WzذalC!ʇC-\"vgz1B!B!B!B!B!Qք _5Θ XB;PM!`!`kFkFk5BX#5BX#5BS#B!B!`M"`p=k".*?-ÑX#l5aΕ tb|`-֨"@Yt5¶X\o(xoˁ5¶X#7R:7X#l5BX#B!B!!`!`kFkFk5FX#5ByY-NF iȧ؉vZڼrt;!TX5c"N9EE߄"iҵpWMȺcWWRRXvú"`蔐 ;Uv׀X2)b3:VHX7՘$ՕY5X!*&)dX Rv7aѢE.$H6ja#;y Ձ7X78_` XYEJk` XZeC7KH` a9ϣu=tH,f w락5K6Ru}jIgXހ5vI7-X78X)._Xk`tRO掹+}ԁ5&Smv֕ Xk`M"f2/8k` IjC Xjy 55$-}]5X5ӍkXkaMd IjsGgQ` ᬃw՟Xk`X{ g Baͪ[+Du^fnp`MRDXk` u`M5Xk`~dQ A$vn낒JY cj H;;N`M"A_k` 3$5n 6XF Ձؕn'&`] u4\n-QsX[mR=N]?E*iNT&"1f兾 =8lt&}JUZy55YWkK_Y76/( ]dpw:*pҌ"H6k8 D&mTFv0};R7:c52:xZR+"nW<mVFmu}IX7՘)X-S+޹۹e8@Viv:]At7ׄDk` H,5;ڬ_N$ x;iڬ hXRAe">$uXWk`XxgZѾ,z]DUmoE;U50ngP5CmU)2^Kf akkRV:\ ;uE|**Ѷ{5D %"u k` suxgwhiÍёHGhz()mu$KDX+Qכ lՕlҵXk`uMgW/aiݵS-WJ^+~^:f-` u1!֡n[R_XWk`.X(*z_hwp?+U/p7r*;a]_Z$Emڬ=咳%Zú:8NښQv45Z\H2` uldmtEr%YP L8XCº !*S*\T{ݣ=MX`] Vƿjj•qZW9t"|T578ڳ3KUZ5̀5V&lSEJsydr-.˰Z:ºGV,c;@@>ZkO_ mQA0mf;T6]jYk`]t İuM\渲fXk`m7u2 ֑6k]tyk[_X蹡KUЍt̋yƯZŀ5֏u-2 P: :qRַ*UuhNop` ͅs᭡ f5Ck'=fnwdh袱W=| ufŵM~4kQg.Nrt973hG .ke-D+o}xM;uXVe|>X #ke]9.LG]VڤȚq:5-گfXWk/՚Y6~ݴ*vjs`uS`]_^D^gˡUjpC_"8nXkFuE.PZh^uM 5nu3"2yR% 5V uKZXWkL`]eh:4GXސrck` kaBxƝXJ+1&XG̫L6N݇9&"6^:yv~eoѾv*=&]"ˉsK3&55ulWf u3`M5ξ8,yՕX&Eo'sX eSfºZc aW W͎Xu^uuh1*sw`]G^k|XPD:GX8Xk"k"kֳ֙?ڬݎ5Xh?uڣ78투=+uݍ 5nˉt` 5Xk`  Xk`mf]UQ` ufX̀uC2uaݻv 5$Y3) Xk`8k-``"yMڣXk` uOT3ºXk`]8p:X7 V5eX XmXk` u=D58Xk/4ګ5ru.y+Xk`=w055Xk` ZԟE/b':Q_yИXke!X<5hX{ywM.g 뒈p:w5X{Ti@aP w1fpѾdj"k"k"ui&nNdY;Y[ڬ5wBXk` u]`,XsWk k{_k` ݇uXi[_c`5Xۏ(Xk` 5$5XvZ|ek[/Kdk` uzk`VظXk`]nL`] =Ky?w ; endstream endobj 1206 0 obj << /Type /XObject /Subtype /Image /Width 655 /Height 835 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 552 /Filter /FlateDecode >> stream x  opmdD endstream endobj 1211 0 obj << /Length 1245 /Filter /FlateDecode >> stream xWKo6 zD4ߒ|,)uR89+-`%m!]IvVԈ8/7d0re45HSbTuNnu0qm6CU} ˦~7ŮȺ½pP~^_[8'hEcbSn?3=aT y*L}ż'*H\e8iÉhWT*#<ܞ*v4:~&6Ҍ-]aׄ}hg[\\tpF"T#{X OL[g^\5YS M!9 7MUeu =2hl}Q[l Pɯ" P1.\=łn=6\G?|EU*/+Fmّ/:6EI1 /~ y@ Q#@&ЧRCh ᩠Lx.BJh9u K9 rvvAͫ/oLPr.00ɨoPDiCce@w* endstream endobj 1208 0 obj << /Type /XObject /Subtype /Image /Width 1200 /Height 900 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 1214 0 R /Length 78820 /Filter /FlateDecode >> stream x | ?M"[T-%)U˷~A[kckEjJZڒ Н֪Fm-jUd8wMD=gϽs9c2@!믿T@V8qbڴiUTqss+^W }קP".]-Rʃf9K6U߲u֪mx…  prr*fK*t$ÃmڴfŋyB#!!W@ZX|yll|޴iSY{ DԏBӕ+WVC&M۷jwܙCoAEǎ8G7ڹs'E4EɃ@!W_A~ҥOժUsE~ BӹsƌSvmXT&ML6j2vG%J-Q{zFq/F rrr͛7m-'N0[ٳg#"":ɓ'_tΖ$$$HwMbŋ0JΝ۾}+{\\!C[Zj˯TRݥ[n5{/}Mg.()))z$J􁽧×ZdI\؊um۶,hѢh)2dؾ}<*Qsγg^fī!CEXde$jG{#7n_PyuI%Tv}t'i7ԋֿvyI`QQ6駟~2ۘ-[9H* ["/7ިVrƤIVZ%=zt``l:4o\uYq>Cy!ҷvzd͖yP͐Oq|;v6ׯWmz X`All+fΜٻwevޝP8Ҳɫ5T\Ƈԣjeʔ;v[*Qٳgs˗՜9o1{GU !I[VzA 9rҦluN:e觟~΁l֬Y2Vvc=W&7޽{^zYuСDÇ.cǎ5kׯzTҮ<(>lJ.]QWWW#[yek?V|0{khԨ:a﫯|tÆ j׷/_nW_h;N2Em-/d˖-I)TRjHT.]̙3V?^DR֮]>]Ν;'+~VoGQcCGY$%L: /^8k֬M6N{eǴ7' r08\N&nw@ѪUe˖]VF@Sfٳzs2ޖtV~w.U~O1cKYhѸq:t"}^:wzI2wh;vXuxԬ O=jOZ[6l` Ms1N9i$y^YyF2T2JXm۶V^-ZjDάAѣG}'~mY'NlҤ{gQݮBݻKv^tt}uuuU.Ruʇ$$$D^vy3gʯ+V4׆yP5hР|Ε+WN0A E:u3W.^8`}ZH$)]e&>}Xm/Y_~I;ww7JNN4h.ga#Èʕ+ggcʗ/h1T2өS'JlPzشmN?*ӧ[H8p@[BULu6j(WEɓ'Mֱc*UI< iluẤjժ"?83Km2t,d.---&&W^5k֔\LI.]d/Bz<==UM9ol ynݺXe|9s]\\྾-[~g-+W']d3J.ݺuŋ;3%2,,,((H\6r:tx뭷,*UT^Cz-[VVnvr OԪF]v:u>>jm^^^j<Ѯ]Lo(iii-Z̃>Ѻub5jp 㕆Co⧟~R׬YcپSNPSԖڂR[ ڴiS= __|E>((yr???[^y)/-Ԗ<(|Ϟ=+ruީof\>m4Y/ OyAm-奶A :ZWlذ5jxzz>ͳzSB\*)奶Ԗڒ"X^@y--rQ.0SF{ (`OO>/ސXFy-?!߱r`A)/EԶ@r| jP^j[ #ve 0A `O yȃS<A< y ȃ@ȃ  Z^@y-#y 2奶XA<Ƞ2b _-EM֭ջo|j%K,{FFFjժL2ŋW-ɃyȃEӉ'M\J777"zyy5hg]~}JJ %ҥK"ʫ<(ooDᅲ-[3ԪUյr-Z?~զiFzam>존.]ʃɃyȃEͅ  T6__w}TH(t|־ O>G>Zu|2.YD-̃k׮UE;SnݺnIJJ"A ) >mժUDDccce޴iStm۶Q|ȃ9%==_֣<0f̘EEGGϞ=[nꡪUʾ? A<ӕ+WV&M۷jwYF_5E~#t...oCGiѢj#_U`THȃ@,::vF<mqTTΝ;)ZE oV wrrZnf׮]#a׮] bF @`!W_1mҥOժUsE~ u$,4;wn̘1kזJjҤɴiӬ&lw]~]"D`__%J׫WoĈG80jԨ///I yfmĉf<{lDDDPPZ,^YO:X~I<82Wիeܳsra*%g#KjAU?A<Ƞڒ=eo%so -YZ. lźm;$$D!d=Ylw۷owwwWvyk֬x5dhIG={aaa7ևׯ_x")O5n׮Gwj|Mh^k׮7o$52Mlb1[lT@D^oQZ5 s-"L4iժUѣGvݶ@^~U5k7?헾k֋fv-~0KP%F;I.;v6ׯWmh X`All+fΜٻwevjȧBJ>!߽{j_|y˸_Ç%$$GX'ʔ)w=v옷tڵ{LL.{Z=.;tPbbqUcǚ}gJUJ2)&zVLNNuO?z':^?'1&lAƍv駟UF `σv!A +>[CFԐ򫯾|tÆ j׷/_nW_h;N2Em-/d˖-I)TRjHT.]̙3V?^DR=]Ν;'+̵ׯ_g:$Bb.#Am=u|||n/eB ݻw뭷,WҠA_+,`XXpEuWqrr 9r$Li&|9ebTT0`y @<)-lH vy|2'NA@44eoUfm۪%OΩ<8a7F4jղG;ypϞ=ƻ;^6m{['&O4HLL\~رcԤ1;CёvRK.m5jSe+{ڵGy;_juZ;vIȃpW*V2 SՑ3gT\Y 4i%?V/ѣG:fykԩS޻ŋfڴiq~_ժUF¸8ٳG]gc`^zIR$55USL1&۷#FٳgƔ-[ݻWj\|%K޹k.='J '옖qFCϖSRR\;^}w}rȑ-[6+V$'')SFA>!Ƈd7ϒ| chh?#eݻ+5j3fE7C2t>Cc/Ia;wV$7k;wntt;v:}f~,gϓṻoܸѬWrrtkܾ}}||̎I*W)_|bbtj>L ˃7iڵKƸVN>$8f4x>HOfyP>l>ጭ<(- B+9T'MXf̸ĝɑ'ON6cǎUTqssx-#j))):ʸ]Raj\oBBBlMR q:KKK|͚5%9;;)S& @bDK.z? /P^=OOOիiӦ#Go-'$$H[4ogJ._>>ըQk׮SNo۶M=jѢZu&o~l%Krc Z$ȃy@^2]ϞٰK/~.[0a0So3J_M ԴiF0ynfze7\yC*i/IG1(Hkny@ȃڪ}7ݸ]EXx5͛k7/צmH8yg2~vq[N.AZ3Ƀ@믿TXxzӗ,YBȃ P™˗hǎ u؟=3顇2~X_}_$M'N6mZppp*U܊/ՠAg}v)TEҥK"ʫ<(ooD{ŴsrZlve?ȃy@aZ4 З_f$F2$'g,Y?ϛ=tlt\cn…  prr*fK*t$A:`0ˆoFʃ<67oj8g״텝;kKNn#ئfw܆ J') >nժUDDccce޴iS}Tm6*Cy0g`޽?GLLT8c y pBίs:-U`rz%#F^2kti- ={LkdyV[dzȃEӧ+WFM4ٷofܹ¯ H~l0_Ed hgcj'p\s6L50>1w)9YT"cǎjH#GEEܹe8_AI_t)yQ.@`z-=9;bbrrO>vl S[zkkޱqަ)^ܴjUzum 5HK3իwO^eGj<)a'{kP [[!HDrrX'c]vI(ŋ9Qٹs~pnjyd_x;AOն1F"Kf5otRWWW.:u:tPe@B<`,VV{ jg<{;}Y'<+ -?Iƍ|P]&A<K(oKO70=3n׹sάe`O{V'nx`ᖒGD{zz>|-2-YZ. lźmH YhQttgkQl߾] %Styk֬x5d7qI'ٳgdddLLt kܸ~A% HS۵kѝ7|S/Zڵk͛'EG٤ԭE l٢"l7xZj*ZO"E4iҪU%G ܿq=7oԬYsܸq~l]^;\֏nZ_}体i ׯWmh X`All+fΜٻwev=Uyfˠ 777B 2}t=fcO%JW^_|TR7<A`Ŋdس}}w4wk] ɃӁteɐ^rʇ6>fVc(Sw}g|رcj|λd˗՜9o1{GU fHROWjՃ[7%GNڔVNԩSOɐ͚53K8\v+ yp޽zeסCK$.cǎ5KJk5 ϘҥKy]ڜʃUT1`Əeҥ^`=_~Qwww."Z}( ><<<88z%Koc?~sl}!X%X}ݻwWV.wƍS,>=Q=7]s~l>bE^իҲ%y?j>`ШQ#+G7lؠ_;˗/ꫯZ}4LSҖeqҤI¸Y Ƀj gΜ@%j%; ]V-|-0FVkypʕ-vY$JHuL5jCn:dVYg'JO4ׁcigrOCciً<8Nߋe$X|y$̾5V6Ν_& Oʞju?2RӰ82|jvjYU7q^ȃ޲eԿS-ZFǏߒ:$BO#c.>>72Nn[o%$$XA+VX0[/ŋa25_P``ȑ#%Ygz?MyP!Vbo~aɞMu5e}%gٳɃ@ ni]YʃְbZ}ع=(N=J 2vmƂb׵`(uG.]+mXۀ#VW9I=(o-25СC6אb胥_hqgOmf*yHIIT> .Ƀ{1.qil>6yr|P]hAbbǎOm6MݺuՑLg!Y^*T̓S~Zjjӧe,_| \ruqvvUrUC5Yfv.v`O ͛oAY5=%n$a1'- }i mOpύmBO'ȑ#wՎ_p\΃ΝS =4+ :Z^hm„ Y$p]qaoLLL6AT3Ncbpg'ɰJsD``JNMs \bEV,J w9u~Reݷo{1hXX%T9US7s.7nP 8ڹ.gpL/?̅<=EPF/ 个U-[V&mV\yqkJJʗ_~hueuV3~~y9sZbE[l8U O (T5a\:ugUޞb2LH+oWr|T^|EaVEA Gn?uW^}4s6;w>\{h`۶i˽O7~6y@rlFf"ŋ 燴6Jij{ ?Oؿṻ[*99YMb5n߾>>>^}U\9;S|De6"@B}M4v1n~UӧO|"4%Y1ͮt0YgFta˃SP.K,A [RܹsmoBTc`P?KjmL)2VTS(7<c%--cBhڵ%KO:tȸXɃɓ'Mֱc*UI<Dؔ[e.cjժ"?83??celw4mi111zY6gg2eH ekӭ_xzyzz^M69r~k8!!AOݺu~[=Tr9sH}}}[l)lU[V.Nf.]u֋/v!IMB///yj׮-;ιsL=4~ț;~xի{xx-[6(((<<8#}||&%QF׮]N*Yo6=Z3g-Z$Z)۠愗ѣʕ+ӳ 3fS=Eq.]W'ݻ.*ݴ<dOejӦM-،)g~_~%{ P~I&x.ZDzꔶxqӅ VeY$y`Ȑ<ȃPQ@3+gu&7nh6xi fNl{ P0T]2i6tlذ_%ܕ)cz}{_tmʔcmA Ƀ(X~׆ FEE?~\?yɓSLVJ}m۶uaʕX7oޔبO8rEFFU gOC72>.^ 'Om^ժeg<}e}yȃrQ ܼysŊlҤ~liϟ2eJnj֬YT)uA}@@#,/v6ڳgOϞ=+V(]Ytb9S{ HOgLcju= ,mЖ3M/jt{ 'y<=BydTC_zIkGM}hd*yȃA<9Z2ݡƇߑ3erc-[6)"< A0AKF889<س_L\ ok[U0#yȃy@9RKFaaط,Zd̻)c#m&ȃy(ԕt3gf'ڹQӦ1c׫~MmK``9GWɃy`"Pб/vZw1uqcLiUۨ(UڂQ.7<-FPXX̓-ZΝ3<8v,[P[Km(`O >}4sfH"Ǐ΃:0S¯cG--^\_#h}_[ȃ+0S/0PK@|R_{N%LHx< \=(VϮ]Eij}TV[h)@!şÇk 5ܱ SnZA~Q.f.P V矧F{J>//Ewi-89Ҩmh&Mr)oa}ʗ:ݔԖR[0S֖-Z[&1C+ˏ?rRFjE%ЦV Q.ZZyI*qc @%rК8cElV ` ȑZ wPӮ/nrb)@ԧ|f̠lJ%rp RT\ZeNF{J-/Ew%0PK=|BmMU{w>ڂQ.7<-VRϮ]_k5ܱWE|-P[ j(`O!Z9|ښvTV o/lXxg>Ԗ2S7#.\VVʕl$*)@dJK1گ(`O tSH 3|nN奶r Oy >}h3cɓ&g$t*[@y-\JRpk/1vcSɒQ`K)@ djGT۷k7O}{ r)HUjfN* r5j P`K)@RrkUy~t*P`K)ό.];4xh+^ܔJUQ.YBK4 R,yӴj@ R@ FSfӳjq嗩m֤gPVKm`OPř m6կ/Km`O$ Zqq1]Fm];˖;2$Ce3Z gΤ({ P_Z2Jdߋ/j5=J\)@qfƝ7uVþ}0۷kALwW/ر#FScx-I%kelԈJ\)@Ѣdϧw%.N+FSfd~ޕdJKsw(/e =ox[\Dm͛/>|'R[FScZqs3SۻUṼKm`O ;sT"ԭsf*ruZi֌J6mbXA%Q.7O0VY0ED1B+fX/hs`O3~<&i|Y%K֮$({ 5mEh*.Ԋٹŋ) 07 RR;M:|O>ъ٤vdP~xm>|'R[FSZlowz^{MaD>|'R[F@ÃW^dI'''f͚?ܹsVÆ %J 7o^jjHHH߿/]t۶mW\ɞ7<-H/bK˖6g8E[C>|'R[ BCCRxqI˗Ԧxyymذ]j/ \\\Tv]~r֭sssS'===(^~Y-T"gwFTjצ$@###I85kT\YEBQD___YJYr͹sT8b"ÇCZ:r,zk"IS];-,\H%rLF`ȃ@ߪԶ`}aXX,ߌL:tȸW^RJIII垞.\`OtSٲZl{cͻv@7n<8u+V%?YW*UJ JT 'ZL qR<**=N2音._6*[@[C~[lQKӧۙOgϞ'fii[7*q, wP0EAѻwoluQOj5kX6ԩ<Լys}g}޽{-׭[WÞkiQרĽ뛑?by?$IdkӦ===W^yE_xLG?| _kP*S{f͌lUT1뫮%ɑ8Aȑ%KC>lu$_L)g<6'!$$tAP*D@RQ@ DA)"ҥH[医)}k3)ysr#">%4w|S~LY?hY^ypm5*U&.!7oΟ?7n|Xd,ԩ=%g"D@ԙ1C|JÆh(sϙ)ch˘ȋA,&::zܹݺu”S~tҭ[/-p>})SFYx`ڴi)zwŋ%lذ)+>6KL#h˘ȋAz R0#oQ?G,W/֭!) hK @OA !. c܋W^1 ^3~ r)<(׈F,Mb\b[gFǎơC@ @Oc"#݇YˤIF6|UcxxIc~}{38u r) .NKZ\1zhws2 DjZ2iJdY-z-^hk Dq#JdV3x]6ٌ7䑽ZDIޱcΝ;]V"zJVˋțO\Ŭ m  hK nݻG9{섉]wy%Jlt)ț^-֣lYn?X'˔ 6-c /ҷo_ &XR.\?~ri)țXm6Y+2k5$ϲeh˘ȋDԬYS9{ZR}]rw۶m^yz dz:Jd/hxz/aCsQ (ܖ%Ly衇o}vF8ˋX%cNєiJ*eT %O< g ϗ/=2Ht\Rq&MS&2R*7Q (,og?apoooz d3g̹Mbb#rŸt)ڠڨVZ5ǏtBOEڵ;""6w1cKh˘ȋDά^L2zu9r$ʕ+/țAJ|mb"uі1mrN>}…$ׯ~:=7̜)Y3]Y#R"2&-Q.= SODb6bP(Yň;wGR/ \p[bccO?m=2Ȁ; A܅e]Ç(ܖ'N<3y}$2RLoDIΝQ( -Z# S <ɓQ"ס2.]B \pCz!!!&M:vXLL =LFb:פ\9ŋQ(}qpٲeȤvQCwߡmnwo^Ж1mr ɓ'O```||<=7(SFǏ?mndbeі1mr )X`pp0=7 DZkFɓphKE^@[\tc߾}qbF\1W~1rسgO@@@V)%(ˈK TЉ@ nȪUBBB7nnݺk׮S<)^ÈC\JHџQ.zy gS ##^C9ȵ*%uy3J4BO\w%KDjU+Q(܍i#**A TAKRGQ.=2&/" m,ZxBhteLE[\z 0##Q.C㏣msgwA[@^%7'66=(οS'ͽ#u4l2&-Q.'׮]7n}gUUV( ÇPr-*uԻDnٳ{7hٲeKO \wBw:v4{1rrJ%n߾'|z'OdɒW^@F)+MGGRGհ!b[1zh)믿&߻sNKe3f =2㏋Pr- ~eArͨ^{~ ˖-SjԨAO׶[%r/V~(܌ x{GOP+Q"ewDz(reg@"ڦdIq7me^thKE^@[\s= vҳSy3BH={6r> ȋhK C=ݫW͛7qڥ2l`Gtgxx8yms/7nUEHMmۆ ȋhK }}}+W|;:t裏>*[ڥ2lH7/W_GK|nXB czy.11 = 8vL,78Ke͙DnرGH͛7_)A~],Fx8J8KeMD\xqݺu_tb)N@^RYJS|XC 'פzD \z 89sb4jNHemDH6,sQ J8 ;KJ傛Qp,Yӓ{\> $enP(AeˊX%>*k8~8z=r? @"&mӧiظQT)Y8r"/-Q.x T*=ySYxzh4[%dĈiǧk׮oߦ@:X^**JU/,,Lo'aÆ ;wq=͢E)E ''L?h @ n@ɒ%~z 8iPlNƀA\z dcPtNưaV?x2zS ݼQ7@ ҥKCBB:t`/SO=|rz ΝPJ8{X@ O?7˰xbg@"&Y313gqaɓC[-Bt^^^W\+WțjCa&ͽ-kt(7z4nЖ(\󄆆s͜9cǎ*TP-ZUV/NO?Maõk<Ğ={"##+'O Λ7ҥPlڄ_&׽;n?2zAAA) LTM9OOOT:yIV3~˫-6nܘ|+t!!!)OO-'b>pJOk%ڔ)SFˠv)UT42שSgڴi)Nׯvm;wN4~LGիڵk#FЅ\At9!ɹs|T~\H夺vj/ڥ2t)FEELѣvmwSO=)RrUw/\@OyN7i"Sn`DJٲeW^}v]n2d6q2жm>H.?}E3g?jJKp! G1"~ĮJ'j b%46ۻ_2\ey+β+Pݕ0jM2k?p>́ž N81:xD[u(ΝJJ81K%_*b dʬ(P@O)Zϟ?_br?(vРA.M2;0L۶mO мX3P‰SG*qB&ٳ/_>//~А;]|Az8poƍY%˖-n۶-y *۷.-Vb&pbڵJLϭCŋU*KmƝd|Ia :tPE|rt}SZu'_>{ RG~\eoܸaމ@̠b„ )<|pڵg̘qQyGyDлw$9p:~zk׮5J7)9esÉ4H*P?.2 Hh??f^^^`TA󾾾~ΡK.&?Ē%Ku`u SNyMNFgkctA[- S*>>ޞ<2\ŊKKiyȨQt7o?]vɓ'_|jk׮?S GٻwodddŕUlذ)ț[صK|aiC֭іvApa4h֭[?0S}半qh,^,XnЖ(\ɓ'+׬Y3~_ Qr¢E#E !G__#&1rU}vŊk޼}VZէO<`Q>z `4O<΍*1r9rH=UT9y$=믋xypzx@̵kF]dɄXbFR)^|QL(t.U9lJ傛p[n޼ȑ#;xmpzLl%r)&71cJ8=iN)sbS yyMj7hEJmY[%&66vϞ=?;SyD 6nD[W 2Rjs"/-Q.*'Nxgi=(=7Mػm]K65mi hK .i-):b9pڵ0(\nݺ)2iҤcǎS :%ÈE aӊQ(\}qpٲep!!, %\sj=q1rȓ'O```||<=@T{E fM>C \p1 ,LOǰhD b ^P(\;zxx۷`t1O<.ń RڡQ.{ hժ=2)/" з޽֥3GQ#"/-Q.V iܸu]FOFx8*q+u)VjZmi hK .^k^0"`GToq jpm]mۤfE[-4BOT=[\CZ(j>,5kdDVOQQQH7իRS S ,QՈ7||rA \z ؠ[7 cƠ RT(@ @On-QRE*wJ +V;wgv@*4j$/Piؐ ~_^T䢬7ʋkT.aruAڶʝ8mi hK ޽{)ZhI[*Uț jPe ں ={J[%W⩧RNx~mll,=7eطm]ӥr 'ЖvD*,XP~"6\pAn6ʔ01reȓ'Opp0=2˹sF d0޽Q(\ES 8 f=7eD6mP(\ݻ{xxرbV1 %J2Tqz(@ .իw-z d+,T.RʡQ. G]dIHHHDDČ3㏣v@J̛'fApYS=9 7A5p=E^Dpwyz-[r9e͛hKE^@[\p?F))1a8B[%>ZN#hKE^@[\N3`Gޔx-q =LѢR۶-ymr)`eq ={+SwߡQ.= ,NᥗP•iLjy [pz ~) LdoQ.'NӧOxxxiE (Я_'OS uu0f J6 %reظqcXXiEIT) :S7%\CG \p N>?~ v=wXfzv *=RMq ~K-wD _$}Js!i)7 +ZW'pqi¯_G\z 1vlvfM;t}jTTuF5̳gSUYG=z1r)nG߾b ˞Mn}BO=xмq56:sB ԗi(Vk+'7#o4er…C*_z뭛7o>}.]:Oe!uƍ'/xhhFW<֭k:g[ơCԟ#w+Ap:6o9~)aK~Ү\1nܸ{v)hI?t ,3A$yjWZ)ي^OY,cFvr9s2Xi^=nu%j~]*GYRN8??f@|>~?딕+W&?e̙3:e):x`=# = [0@YT2ȑݡŋ~)l)E)FrZ+?EժU1bJVڱcΞ=#}Q.]ThQUNݺu-_wvJ%K轿NyuիW0a{ezJ Dv FMѯq!ۧOg}M^ʹ#P^G=za1ym\$LY2VK.o٣VT)B|˖-uFS| իM6ѣG8[Gz'R)HuB`G^4j;8S#o^ 0M_dlw$nn$ O"/0,0& /m,~2W-ooܸRoݺeI̛7o aJ>}S9sf1bD ]iٕPOp/yBkdݱ*W LWfX/ZlݹIcܮʕl#w%Yv%ɀPؕ0jM2@,o7lؠ\C=0OXXw嗵M8qb]9u(Ixتwxn>L.]jVPÇwѰ9w.Jd)\<Ν;ڷ!CX2xyy/ 4H{4eݺu޻bŊ{'O={V,\PٳޱO [}Ss۷͔(SoehFt4 %\  Ӽ˖-ӫmL0JW)kNorW&޵|Ϟ=7{n=_~<fR>Oz 9yR\o(qcǔ_ߊ̡qsjVFf;vJݻw&j jԩcs=xO?0p֣ T FO>~U\@B/ʕ,_2E L5 WEt vܩ|:vx…믿΀yhBOs -K Y&aСCUz޼y$ V^^^d+V R\t'11Tg&?^Dl%JCLC%b„ fX\Xu֩ *g~$ϟ?_P!K}jǎ*SLQ/= [q͛;~ݳ܇Mg+~ܘGj3UI =p@%vժU-3$dyvƍo޼IOQޑ#t+T-s0\oBkih@_)Va!ym>|XFJ . :4""߿zoV })SO``<0m4S7kQNP[NIۜ&g,,(ӧ2֭"bb h"**jCڠ#<&Y3q|RJje@/ߟa!ymrHrǿmz $Jq+W:Ԏów)L7ByI3\b>y:bs4BO/`./WRׯs?wS6~Anv9Xmޢѣ@ .KVZՠAP='=qyIEoRd@qV֞rNFt(3i1d…E_~A \px{S ->ށ'6k_oEfʕEիQ(\ =<<~iz $BgYcK})'7R3A"ܹ(@ .F| ,HOD,]*;ț7eJG?:wA}}X mڈ&Q.y@";{Md۷ DŽ5umnѣ(8bC.+|rŋSy1i[g8cPs[m";wm}V=h- . ͰD;v? Q~[n`GD+.,_ ??YAQ99rjhnEؾ=C.-zqz $b _z)ż9ѣjD'szTmp@ S߿s.`)$5*4m*/nt$RŜ j7,_V\p:ܽ{wLL =lӮ'f7fQ}aNnͱ/r='8زNJ/ooQ ĉ} O8hw2Gf8v.,/dg EOۥ渶oi_mԯ/F2$rЖ(7ٛbTDț pK*"mo7SRɓ'}ƲeQjC.傳p+;VNU+XahR7a!ymrY?Z m/\0nܐ3׏:.X mY Md=a!ymr鈏?t;_Kϙ<#f L^*Q{4Hdz% ,UJؿbEe }"O|ߵB9R}+2uDX1 mK5aaO6lh] c<>E&Mڴ;}(r?ǎ0`@DDD@@%… cǎ}7z È7$?|8ռoe5S5kʬ,NG֯0b՟"gLMv&]r!Yzuppp™dYr| =L]3իmCNuL;g}._k1%gT ʅ\˱c˧<`˖--Zbԩj /@OG%Iu۷y%oɒބqޙ8aTQ7]8{ \pv޳>*T((W[gGC[ @bCCC-om0oooz 0#Ɋ ᶁڵ%NOFm@J\h*Um\dž$/IUNōLbxwVVHc`6!{S֮Yc޽7~<⚄_LcGc"?$]H}P/nOHw]l>[͕hhHH޼y^KT& J0\~ʹ.\0cǜ U՟"ʎyxRS&JWVN+ؼ,%J3o-ە*S={!\XԞKQ/D~\j!+k./.-y&LWÆg\xQ,ڨkyJO*bEUg]f9"Ӧ%=}|8  SuyxxԩS&3g}|^qڸ@*''1P.!!F^bp)Uj,WK{+O{ زy>'de,|\x.MZH+(e,(/w(W[!.,SPVlST﫽~)c|b ֯o?/~Z5*{u3=zy)H">sնjb??/z>1c #Gĉ[9E}}7޻(XPNѢTj/n|_:׍!CLsWѵi^}XxI{:Ejie>x]Hm4,ScÆRz}bܹ|kBfy5~]2(kfIWkg#@9ʄ) j't"]V^o)NsgSmEHz ONMtߟf.^,G >|o޼yIի`|Μ9CO_Kssr{bcͻxNckQ<3hܹ reRs1iq}M5{e_?jU_T2?MjܹSJ奪0R2͢EIo/ۺvxum3c^T3.eKMիRO[OϦ-Mb0-^5[eoiA̙3JQ(RJQK,`o\s5c(EڐN b1IL#.(Sӷy5jjѨQ"lߦ{D遁u -[ƄK98bQ'{03fs:ӦsU>女O(K9 _nܠzez$C%[dl,+e}HTT2X&Jyk\b}ʔ)oeEm\ӯU*U]UXѲOg׋,W7ߴ·ӹsZO!t~Fu3g^U^Ŋrr%5k6TiAdNǎ۫Wݻ9r͚56{-y!ۺDYmܴIhB'ӥmh|?I:ϰpѪ6;wW뉻w S0#!w~jN7jAi=voMb !/m͙7nH~~2,#rb˶  m`wUyUKr;NF4IbbISsu?,%?ذmlykoI֭3]NtV9M}ӧ[{p\1TFr8IyŔo-Q.=[xx,<ml)} p'KiCta!.Hv""V䏼WR8yX&M5U=,#2'{>f[obIM(;S9FhGC[\z k?Y>~v 0\]Ge59wXOea ܶ͘Qc=2*~1sQ.}nԩS\@Op֬1TrV1^6q%jf܌'d%sIɲgd-+9jRMSIj?p'xg==9;cǎe!V[$ߣyuAu /D;s? iXBC7#9>ڒ%ڵH҈WJ'jWfѣGǟbNOwaXkVBOYܿokޏ4.mz;ernHcI#N![RZ5e_|Ez ӣo{yː93={dZ{UUt|im'RƝ'QNT&Q.իgRpSy2˷5Y+Ȟ]Vk)estʕQ(@NOˋ9/oz__CǟIFnN{wX>3MWkP$a9=qBZStі(@ɓyZ]2yX˷~2iDWn#_P!E'Âj[ɓ͡CR._xxxL0-ѣIoRl`<>f3K&eloc7Mu7-FmǍ۰l<)׮*MmrmڵgIQ8~kujeɬ.bחKV$w߹5k7z9@F/ i>- OG\pO"Qd6mݜx$ #mxzzSr5ۛ ^-ˢ|G1HJ?t}{o:|8-25p`$ ٴIOT A݊+Q(\Xݼ9e/]Z z|gsr`//@ @ON̟Gȶ[F8p3g̸1tђ'ƅ )Dj|y=YVqDzSDk˯˅a:)L{7W8tH6BrG4>pZƎe(rRޫW͠ 3K/9$Ə|2;dO׮Q[4}<V۽{~LjEX.S)g̛'o+WvI<8bD=˗˞f͌5kdR%h׮_D eXpZmUwX£GDග{-}i/Kt.MG 7k׮=;… [._< z7,TSr e kV+^-K;'NHQ=&M?-ܥB>Lo4Mpff4Rr@ nH2e<==\6y$'UffMf@nokSfH{.Mw!!Fl,a]^rhKE[\p7>e{=vΝ*bŊ:u'NLc7nX|1c|%J菏9f#FC`[ڶm[P!//hbժUFxW?zyen2ct+?>l=?Hy{gq{/^2,8Wªؓ'ApyN:ώ;֒2i$:ħG'wvFJѣGdڱSgՠKҿi/`T*o!omZe'NWMZ?]zb.ӡC=zz=jF% )89+KS+n ѣu6hРu֥QQQoڴ!Cϟl]~QƆ {Nu=zhK_S58MY)Su׬17z5&_z^YH/Я]++WfTV-Ir\QSܱr?^)m?+A `FSdI:uc?裏%JJ(e@e7ZI?;lu /0׌OIĠAٗ^J㏒d>mԨ=9eL- ڼysylٲaÆ@?g}pv;HSxʬYM֡9Сѿ&sHSsz0/D&o\AE2"¦MTprw?=j n " ~\e .rRJ)~?ԦٳIvѽDeCXSۜ[F۴_LGmT^5nsdcFn]?UWRyicpۨtY˗/7oruW[^0.EժUKmѢ=\ eT5+QMFr+^gO1"g-t}+gd-m.}q:uD%Khf}P^xUT|}}*P@}GJyWdK%ggVZիW@Yw?4}~e3fkNV<7KzRݬ5+Vד&nt6)NgŰN~$`ۤ _)yM2EBYzu͚5+iԨJW͢~ilٲdIJ4#|%:3w0!?pٮQZl^3Fvu"GX_[5InB~RKAQQ hKPQkP?eɒ%!22Ry(lj*z ¬6u떾3((1bD ]iٕPOp䮃ݤ)_޺kڴ ,ZT%q; źuIvG pbL܋z~]ItvܺK;ڴe*frWһ+aԚl}f)X޽{NZ}_mIt\gb* ɔ[Qb&͘vZ[F{|2?ɀ 2uc=f7^POmsסCCV}@WV-&&&azfT:V\xM͛g77X6s d^~Y|PM9[Lk׶wv uб#\N ̅ Ξ={9xʩ ٹS°SW^ֈntI0?kٳ͉I\I3$DvMwŊN…#BׯEЖ(|]իr}&;']"6u9%-C^9u+9]m2Xr)et <Ɲ˚af3`A&dxV\He ܺ>E^wǐk2/6ӧMC$\z @V1bD\ݻAVXQre6on|cs&>fR>T#"^P| ==_/EU""SḀS%ԩ&!rOO2gI3_dxy4j=fx9Ʉu񡽂q9t,[&C+IL/Pr)Ui'װad[@kbǎ;I3jִZyV_ uZx1p(S^v}FƜ9³傛MS^8~ԨTm$ΞOȭLkK/پaCڽamKDz. EpNJ}횱tl[3Q.8a„ :t 8qY)x{N홊'Oʢ? }Ԛzu׬4]Ve`Xp5mk4 l G"/-Q.0g"Ej׮}-z 0g7O?-gJ%K̰zx??Mk#6W<iЖ(%Kxxx;MnM)y7lBx"Qb|Q]kMf0Ň{O<Mלõo_,Âi;k1n}9*)cT ~z%&::ǧ)cI'*_d϶u ˑ _j̅"7G pu_fz53Q.4q)|4m*ٙ*d~y19tB\W (xz2,_%չ}[|Y,a r!sQ_kGn-\BΝsaNLb|A裒e 8t|]qI"Q.0gϞmܸ/= ][­oɪ+U7mcd4H5jHᆪK:[i& Q.n"##3<ӨQe===WXAOnUpkʬ*)kW޺U$3W˖)|Z/ImdĘ[1B}Ϟ(@ NMp)|>_L/YRn7Oz(|Y{U,6EuCEB p&LF߾=JSt2p9s\|iOnmڔU^f_7 `Ye]IGi[oZϤ?0Sag<88@{hgwc={a{]ch (v 曗w8=}ٛ=̽fJօ (oŊ"!ؑvƎem۪ȑ]w@Sfs Zn!h{7U!/r@K6VH̭cQ?Ѡ;Qttw:>o^T]Xoj-r1V.h)"ݻ'Ξ?P̴AcKp۶./IR< i eOX ~ϟ{Z qbTAoo!e9?ԠZTrNx('s  !1d޽r!Cݻ-& +Y~-ݴX9s.B6r91:5Nt%@BB,$]J+X/)R@K1"R6OJ(.Ef!T9l^,ΝIVP-$$mOzȬ. ,ٳgwrrBK›7d_iɒŅ[ڿ?__v <|;yUW"IC F aURoh +X){={,Z @ v]FRŅoҠDn8})<=Qu%7 *[H`ڊzh +X#ޙ2e*Z؎a)8!}5c8JQD0;Fs>|o߿?50ɞ=$HbP$<5߼9V.Rn߾ƽBww={h)vI_k z͞=FbZP7# A ܠrrڵk$GY\Zk޼?[m?.]"A2e q>$IXp0V.Fg0UTGV2EZݻkBe@1et,z۰!svX2 #:0;;jϞA `q˘1+={%4HiaAZǎvĔ~h@V.+Sߖ˗R(H$0rA#eʔvvvh)h_=AjipӰ!ϙC(OP$z7oA5-x;'N-Cŋ['|OƩVMM@B_ j% 즑#GbOO\\依Âe Nٲ߬wdc(*ǏSHi8 Z Qc%wnYҹ3߂w4oNF?lG@Dݙ2Jׯ,yXܾ-SdIY)7΂75j: t mW/gH(0?D`t(\R!\ҥS#x]CGʻ;u U8bo_Tht XYԬ y Z zx`={hVE9ضF_ngBPǨƙ8UBujI2 -\Mƌ3v5 5k%+% Z I[z{L8Ȳd_a9fbǏvGK$C yZ-;v b+vMzY[+hJ/\X]]oOEÂ|'Y27{_a>M%@eKj Rw!r@K@eNuFo}^8tE[1q`ܼIN %@gVǗ/_v9u?ɓh)3Ǖ;XA" ^[(^ŠRӧXDaΒ%jԀgժUWܹsaEK*"^LCKmy!I?& \@7S h~}T]SQV\[Hڞ9C!cF -\A6n}"E{{{K.lŋV-ԉ^hn?h9y7mRsgT]S AyϞ[H~H!e}z{C^maѽ{wM:ՠ|֭˗^l޼-7Tw)\4ޗFDՍSh7o[CAIh y 4CRw5 2zvڕh-@ڰA[U0__yo8n]{q.͝- Iyx@ kԈ̙PX@kO1:ܸqC,<(͛<h)Ĕ/Q"kЀ=p@NӴ -&[lvvv^^^E`gc]\\R)u԰~~d8Mb;qB/u{|mQm[r!Z"8%ONMb+h poJݻwEu#^^`AbСdͻ[Z5dV}ÆA B)UD+X͛7saʔ)w9ydyk֬ᇚ7o""DN6dMMyGE-:k=z[YP4- -\BBB*V(B# u̙ǔʕ>2lq2L?ZErey}ݺum].]v-޿-)"}Cvƍ5jݭ^}.% A]Gmq.KpErV:黛7o޲eˣGݻw8p-3 B+pH-Kwu+e iX@- Br@K @m rǜIᅦ##`>HPhlW}Ξ% ]]y1 Z H(޵!kV UE;wH)ahߞ;V.h) ..ToW}^$ mmDƬY<6-$D.^$[(sf`t^^=@Çm-zx@`pBiSޠ GH2ڂW,yQu\R' #CHm˭3a}#ĎWQ-@0dH)kbjT]h +}?aÆٲenj%O>իW\흜J*hѢl׮]֬Y...+W޸q#Z zx#Rӕ/My_ ߴjYM͛aZ-@phԈW/-ϫFՅ ǎ رc;өR٥HQ\Rj*vؑ$Iq~ʔ)K(oժUH,Z 0VS6}=ŔIɿaVjYLh HA8~xԩW>dȐ?3SLo߾͐!?ŋ$ `+ܹwM4)?T|{???wwwN0-hyiDww]bJjڵP0Se17o[@G~ٳG:M<# &M#o-oѢ/Ϝ9ny׮]yӻwRF2_ݝ>Mw3'Ҹ1)9w.:e ?1cF~cǎaÆ)Ǐ78bph)@EOϞڼ; ^Hq - H7uְG'~DJɞ={Ġ žƏ6k -hd M_tw: DIAf *$(pӦMF' ,*%SL%G ?,L.dͻ۸R%|O1vsC?Oo: H̙3ǰGgϞ-*KKҤIc׏M*Z:" Z3c6]NTݘ2{6)Th 7n0j'?޼,<s m7qgFdqٳgC>|8?b"k)a !@}xžsG}[>?/qS&P/ڮ\IJ֬nچNR`nnLD m ZÚ(aqww)t<iPޠ 6-]v o;vp5~@g޽k'7 CҵZX?hqլ\9mRdܩ;}HjSkQR’%S*A|7o,<>OOϰG dOfԩēiڴ)mѣgߓsi!*fsDH|paVl0}r7o&lhڵ%K*%{Þ_@~yh)@+dDvƍmΜCwdnjS.\ %dDѴlqc`-ACLwrr⇆F~ kRY9ɒkvo.\JJo9=z'sfAaÆɓ'nH5(Q;kٲ%/Ϝ9Pн{w^>eh) N9uJ9B2X lff7S<=Iɔ)&0fkA?xׯ߼y_]]]6x`秜 2u/^%_~]`ҥ'߻w/YdP > IΦ8q"Z 3gFΓ'{B{{kfwjr>)<9.TʣGA@ G;ON:8 Vwܙ4iRqKDĢ֭[X#z(Cn|ۿ}(uhGTmތSHIGGt oWٳ!/.?9r ^zΝɩtҋ-2PpmTr瑻+W?Rkjl^[[~Ŷ#޽1sgHt 62|(_3~ 'ƪ"f 4Pucʛ7Ҷ DnFV8Er@K]ȶqsc:2$wǦMtkek3>>Ҷb\z%I‚ r@KVd۴oO΀vʥ4n-TL#Q,y|m\RS&6cx oVak3AA@Tș,JH1 Z E?Kw4`;vN*UVf6lmIϡ&P=OF-X7,Q"vPͫP]K;իk~s[;yߜpt$I&м9;͙Cͦ^=bu +`>U-= P˖5r‹ht3Ē!!ޞcT]sR6W[VHDx&<{X('O)T޽)y?}w닪kNBe-1@=xzPu-\RƑ}iߟyLS7Loќ4h@.\%b} r  z+QZ#H8oD[tS)S+43͚%:PCޝ^[-hGʶmYPi9-VHӡ1`(u*V.h)@Ki& xHkt [43ڑ&A bŪ?n`ǐM+Vķhft!aǎĀ+E[4x{zLϞ$ȑP(3}H\R,oB}}bggNJަ ׏2[h3vR-˗U-=rE*IX#Fнkf&a @ƌV<yQu\RkۥScI^(#= mݫ6U^T]h +Lb4iXa_ }"̘1$l׮PsPi۶-h(ocu KS|ffC(9hQF Z "5k}̞<{|=ujZ̋7nJ`Q\RVȓ;Xpt*[4?g͛C oK?x:$-X ck~Cpb|ҶQ#(9OX6kk+`y>$ёC+zuKE*矡`nRJe.r@K&2NRy3d?{UڮZE֬ mB[sp=U@^T]+E ծm|InkPu_uׯ}#5^^,sfvNyQu\R[C#k+!yQscnB/mB[s [}B[X!!5kF6̙FcNdI(Yɞ]hV.h)y7+Vln[!}ҽ2Vػ-ZJ`VU)\R@\s+\dXVHts໌$yJ`VWO-1ϭ* ?q^K{߾ep8ɛ??,XoA`Y)*u?]8fEX)7W.(Yt*h`&ƝٷO.Iv=w+liPȒ$aj=;TV.h)7ISukWAԉ^K+UR8cFhښbXz?X~yQu!,\RիKSݺ]:_>z͚6͛p-57Sdd,[ {Cȋ +>n(P=ȿ*yW(dNj{.) mB[ssP;y٭[Ύ@^T]X7HAv'{Һn_!.xNJ;t?|qq#Tra__i|hug0|ųg v;0h׮Pʅ Z <=pq{]vǫWR`@,n?XTra"ի1CpsFzR'z/Eb %'ws@-+$۷ʕd{TnoZOfI 1e\/;[Xjo_ʲ\Z C61ផҼj:# @- #<^4`T7/5H`Rb_^+~CLfuF0j5RMQgc%t [֋0s&[v~9NΛyQuaRNNN &S}ž=(oɒRQgc&K& Uh ml1k˕v6m]ox[+۝;UV.-%sjTΝc EbXڴs5jccD۷Ɔ 2Huv>`R:KU*O&LM6$-g]~mE>x0jC#\%5x.ĉ/Nժ%-+X|d!gICE r"cF=KJsZe@ R2g&/^ KG%C3ؿ?W``sRwJPl%KFtGJ99wnk 7߿:dNR9%e(a6c  @KI N@޼cz/cƈFTaٲ')ƎQEa, y'OB ѣվzn*QIV+A\a^Eڞ=9RЛzfט-[Z8SFR= %\g"hT +Eycmڐ=0mZҰmh<ȯb Fy߾G` *lW…I ^\Bժ"{%ٛ7UV.h)h)×/O/:֫gAӓD֭iQF 39q? ,>~L#jk\Tik@۸ cFZ=caٳ,ujjE8 -h6GU*UkSpBֱ#s7sKXʆ`n+oHe 7@T2eHۡ-qźujt^͛%\ -RXwK֖]ˋ1x@5=]9(~^ CUȒ).XCŘ9dP-RX}xDZV/y"N2,) 2n6n,˗).V^J d/\X R(O,좚?/mZqD,P}ԩV N HD ]^JDY~5`,YGE٨]HW䃺lܨz!C@5ݺE;wT˖rpp@} 7 b0M~J֫{X iR,H;GD7okxH7ٳgի5MJ,IۗmB;͛k5P;JƍS٢E-X?iK;MÃ^7oK#s=ooYm4(Otk=o^ʵ6--TŃi@.+[5h*lIϜ % ;vuC+`Fyc룟>dBBdT}ǿΝ~sT~qr#W ^H;Cdc}''Ɩ/O#EW\P1X+̛=< QFowHosSɸqqWuw'Gj=ؘ1jG9aEՅ?FDe2ƿaFdvؑ$I~]ʔ)K(}bVeJ@#`Μ9M9ݻI&/_^q.\ ۿ߸?ִM$!*yGOFe;$ J1=sN??H`E`-ə3g-ڵ+/wrrzZJ|Cwp{vb,.]beʰ.]TG̙Hx͚ƠS+,{Ul-[(a 64J1BBXllv>H`-  .20e~%pO-h48Zҡ*,]bD莊EsgɝAΜ9zqקu3%*% EНQdtU! }3e ޶-@~lmp# '.]:;TСC z}iҤ19GS)sR#20WWD{b"jT `}Z! mޜ֕h O^erMDielLeKh )Z5YL"E+EՅ?@\rСiӦyzz~B<<ɘ^TlؑLO^ʕc>>lV4I)bTH1E\9XA׮ %0,I }Fс0 iN:ujd6mx2uZ] oMSFҥS 1Stve7[s1\]^}[Z5d,gUrׯRqʺu|jPmS(e]jK$rA+7 (6@K2~SS Ǎm[o65&O("xuw׋J_/Ћ÷QM" <#:u(Шx 8aF~2FbϧiS` *ٓJVoG?jNhH^>adF>|`7n?{tSy򨯇SA۸Qy?X2S\};)_4@\";)SmBi,!!! 6]O-[䅙3g={h)e&A*P򷛓(^x́mTaP4̘_6eQw7(Fvq? xaɒ%/_c>}t͚5x_Ϟ=uϿw^dxy DQ??cNJ'AKj/cs 8Z)_-m ɷzۑ#h#͋;]}ٻzm`+]l^֍ ukÐ< nJxmh-)#Jr$:8'N6-o:vCKvܙ4iRqKDɭዖbюȜk ա/]OΝmIŦ2teˢbbѣf A Ww T/P@M!M&Uu+`E@[mΊz'y?0;fj֬wSH;uUon׮r'ZJᅟӭCb:(dLByB6vڴhi1Ƿu"8+3Y3|撗!CohEIZɓ>Ҁj'OY~mc -q?^P],P=W){دYSд);7?nYi&)…˛,?ع3;EP]݋iYDvmՂ̙zPua,2g@b]2%={F'ԭY`FFEWnrRԩNժl`x֭$ѲׯiB e7},i֌r+۴lQЀ5U# &*[E}8YKTwn⿘bRD@|YdoE_;;w^+&+ -ŝ; jۗԬI|0V.h)  fO?{NɜYޙ6-'_D zkW4o@۹s5a< g^[bQ[d,ŋɧSM:;fsFG2IJpGɒs=38" ;7{N>sXXD%O*UY)1pY𑢝:Jv-VB.\]7?_?B'5qbrUwwr){v#VYQ,koiԈga!_~v<ϟU\R@,2u*֩9;@^D E01O!p׮ZJtԹɬ[GeȏR7'pP ?6˳VP,˗f{2] 7%T&N<X~~p'O >N)D`31>,!mZz [05,`"?,4?|X-:Kt}O[[zqy6_o<Y|СC[m- ԒSM^=胪 +=cJ6lJZ3vJ4o@c|xԪ% թ[pvcLe,YZ0<<4N>~_PcDz@T#,Z~)EB@[K$?L胪 +=|{dgrXwf41PC >Dpp@ŬQn~>tݻ7E|&ysn}ܗZհz QI|+[> U\RfNv8N>]4i*ufɉ6ɓ"1 {PLaNW˕i˕Υu•{'> T]c(9AĶ`AD'+c|ؑƠ=< A \[m- oɒr3n@"BT]X?,i’%J8P-,QHNLAݴI-PފA:(8_6g4# 'O ̆ 嵺#dMM)(7+tr/`<|yZ}|hq6ӧg;vRB//Ox=}J;[ư^ E7ݿ?r@Kf@̰3߾5L} V-kvPn 4Q%Kx޲[~Mwa0pzn# <U;"{v֬ztxh̭͛rWgODAx;#Hg^zs>$r@K'9S/ 0/Z\HW:RЎbב#\X( ի[7F ={-ZΌz7.yҋÇzo4jZޣGq)._VC4#;m+V9r@KI;'5{-H,WLjF}6XJF~˗Tت+Ba4tUX&?Y 2<ث&3qZtcTD%kFtT81Vc.^َzr@KADORApO Ai͚zKF3ׯ1Fr椝U>8m~Mc aWУѵ>>ϟ-Z;q'zX勪 + (KGGD) VBdC6}+VLό߼9jxzmLGu*!eΜ3g"m<0N[[`~H9#@[m"/6wgRVfT]XS%%}z=ׯM*#z*ہQL}TlÆׯe;wP'!.{j)>}:1r$f+y+]X!BmjeٻK ^3V=zE5DyAUݘ| VMI4?b٫V򊅾|:2-=|xy&w ÌM'O&,Cr1r$sՒKs ~ϝx%Kʷ 2W)&%v6jxkdbQMc a =\R1&EDŚUw_5 >}buN~q>EYj*~u+:а!9dʤ7bϫuku\ȯzL*aKme@+J` ˘^??p+V.h) ??m8֖tet v)^kr/Y2#Ex{֪E!_p#Ӷ-%W $K2e2iW!1YeVΝF0?lX!M"1$@äN͎" ZJ~ӎkyJy#9:ŅNLJ]D5+wf͢r89;cB'j|B*ڮE4p<4t]+ ߿|E7ڶhJthz 4&![` +_єO۫ j3])b,)ߊ4ZׯSYTjj&M8ܕkoTsʋ^('øytF~ "ˡCWl'm;w6c?~hmye񁘨r@KO,X@oÇ뙲۫=|~,gNsQ3NmHA^tF|}6Ν3_(nnxCP8ю\DM~ rS\iIѣeT+իg҅J mQoUw_ +xĈFt7i\>=(_2NGU2m(^0f :t)ծ-'s _u+ؤA6v{7}̠Q7YhCxWJ xĦHV: 5<_Wϟ)Pr'fd2"u문'm y=(W]fUvU%J@[m5/,Sr@Krp.nnlXY.l(2liĆ<Zƍ闲ohŋ|oM_f}{{SoZ5I C"@nd~Q8m޾UC㚎X`7/V~ +rqa?~slpMk3fVF|OSؙ3"E"l= )EŇ|75p>gL!9:^e,~b>V6i;l):0mZh ;k׮UV.h)ᯉ}*bSv¦ _lU{:OT6oYK_'N?&N]fBaٲء&r}T6j֤>_>|o ^ +fԲ%r@Ktݿ]b$9WyԒU'(v7<׬3~WتUQ` y3៬TRQH?V16˖Q[\߼w3ֶ-͌V.h)VwBh[$(((:k=bMH2@DTҨE [2#ϟ]Tiт/[nGg+BQzJml{XZ.\ߪ9sd>}  Z b!OϪVe͞miЦY6wg@AHe&J$;w3BBX,Q^BK@sZ|u 1ch %6Hw}IwpHj=zi9رldm]Rlv_ܣGE`y=x@߼?-%nA`~^n)666*oTY햳9]~ej,4)VnߟU C -ķoi]…'E % My?Ooe\Vʰr@K,I<+W~{_~WN$=+0;>CZ'Oz`)SBQI2ԈFb9sVߟ)"=/_;-rhCƽvȃ~\qϮhQJ},YX4ۛҖ$ lggrܽ-QԨA`6nՋ HǏgӦѴ%K(XM9it EÇ X+`Ύ~> }tnd?wB֮^=44N%*͋3/8`A -'ɍh!!ln mQoxCl4[r0<`nQmvۿf'h[ef;@sSsVqcVQ+NJgOiM]]iYHFu [@k*ӥ{ȓ*DwU,Q٨Ow޽[;f[p[}[{ꔜ[{[c[ +؀p ׬@o"_ /;dO)ٗ/#7YμyX151m`El2Ju mQo54=f>znfW a͛)ҥl\w6r$4MQ۶Y3VWHs? '#˔MpPloLJ>} Znꈹw˹ɹ^^rn-{,=V.h)tV>Q$3g!lĈo{f|=oUNuk ,^cGTF'6pSL,[Μt((Oj^[n3Wؓ'dH\.\({ٶmlzbk3gRww[ۯ֍ֶl6ULEiQ46mZd..~skWsk[skMέ:Uέ]Zέo[rbP3 ҬV{Zz*wtQϟi|SIVѨD3q]3dV;С B oR+aݺEљOڹSέ]Xέ7 @ ;v$åI_2?HĹٓ-ͭ実wx`7Gg§[7[}d`UPCyBr^k QGvZ7^|.b+Vzy ꔿ|"/_R{=9?w8AskwLkҤyhn l t<_&QUJE+o V^)6f2t1-oNNr,Yh6S>IH8poEe 6մ3~lYѫ-E a'1`z꾣4 h).n>>+W"i%ofIRpVx|ңGQSVf0&UTFƙ3~p>/Tp-˓'44sfzzmOb :8r$ guE@b䛁.8;_2UKij%ׯ1q#Ci XVF7ii$Le?5k[̅X#ߟX45|yيEiahADlm)- s$uʹs@`~'yydnnҴz*\ӦQfviR^&\8IN3Ef)t ҥ^%1 Jg S3 MD K)f[<<"X{\LȅSz]iubaJ!λgy@p:i@ +mfbڠ5WNZB®+l*ӼVLO罽c/*%b({.)7ժ#)S3rfCS'3ovlpK/sf:rڼe oӨ#FK/\H 51"@H1pxoQ-cgGO4D=E9.-su Uk֤H;;>]BsmO#Zè˨B[1uQ}1Q,{[XEJuϟSjG #zӳ6gNL;<Տ3h v(!o`ː/yr=OuKzחٯ;FơS~#?6n$׏;{VVWفhr :5v,y;SܻڵYٲ`*{v*=jh89r2kHns091@ymGtݻwÆ +Pc$I)2yd㜨- Iɘ1dyo֖d]zn3{'q/JOM[OWn@Ud2ދɽ*ggyKLb6B.^:Vnַ/^;jMy:'_iq{,9^zfz_r9VPy mAr# [#l4Υ0Cݩ׭+BMR#M̬YiC&4+tJ ?SQo!/MXwuuu )iҤb~xu-epZ=wV3td':۞_ICk= \׵cqHw~Y|棡 H[2*ŪVU/U8~͵|Mf͡8\ٱ[RRҫWOlc,Mr_k10됢HWhaˤ?}FpQDK႖;KX5ǁ1K2|9feeӵ/CB77oAA֮+d6OPźȃ#ob2o<=Jv,Rs.^=1oh˼Em/9MVf9˱Uzv^:-_e1|mSaӦg9u*o^!8غ#/ *:IvOҷl-vK[[>oof$--ZbNȿ7,{R_}| K[k\[d'n9[\xԲda-Z!Zt |R[ڞ=kFhzKymPuf ypAZje?)((H&uu:PBd5--X~4yddhk ;\sOYl͗r`zb_m鯼^1mz&[߉_=oY>}LW[Nw\XJ*4_@-o%>Ѹ_/5nHysҥK4i^{M&aa) EwzGXmSokgjB&л#}&?a^>zLẴԛ"% 1Zz]{LXgQ>߂$=,7&9_m72zbc> 34+Vn/ZqRW9YG5Q`e""/#cijV%J.*W<3jr:Ǐ繮Ϟ|2tȃbbbGѧOS*c΃}7=6JǶKign~WO_4`gOz^L#N|%骥঺n\Vk{/׮YosepNO=1ijik?y%<y|ͫVYO}#܌_}hcu-|R[vVH`m޼:?__'FmAy۵ ȃCK,qI-[SLRY|jX\ifdfOy)…%]u(uk~u=nիzzjҪeE}6p>'%a󞱔o>sW^/ꑽ;avz]j=u:\٦M֫gΤ[g&lzkպ}/ Yo)/8Æ"##5>|~ڵkum2K?Ƀ ;_ ;vh[ziӦe2 ueĉ꫙\opOkذѧuF3sLcnuQ%$''׬YS_ٲetUn,X[[nM~uСPpaOOO]vmF[BB!CjԨQ`B խ[?z9W!19P||ٳw.5-e l߾ҥK3wy7.-8wȑ#~"E(PdɒÆ p͍oCe˖rUTI7of͎UV5Jjh|9|L׷oߠ 777Y6l矧:~ 7H/VXXXؼy}UM֩S` +Vڵ {W]>}CΔĉ~eM*ߪUѣGP[@ʕӿ)Y%9mHb6FC?~ڔ(QBxm۶5SbccRd%j~~~G^fftXQ}2fiƍ1Rd-:FX,]T7&2ō͸d4j?n Ӽ&7n{W]3IC*UtGV<^H///cM>ŋ- _^F YLCLCH#Gh_of瞣wx4nX? uզMSNw6wޱc3uɓϿgϞ'NƣG>n<@mydwܹҥK RI]MK/d5t:tHZ?}wƌCmld9w\\Çk{w"fW\ B=`/otoW^ߴQ*}CJ- $_i͛kRlGhRܘ2eÇ;̃%QǤǶjРԤVZ6W.vJm|B 2KB]Kmcccgʔ)cs@)R$!! Ϟ={.G-Zڵky/k[oiwxy]ɓ'oYf㏳pV]+ RyڹsnCy矲󋏏ׯ u'|ׯ_g[qgUZU{OBɤ6mP[d')_/LJLL" b8Ql@3gΤ[pń!!!{0R^jۿZ~,"Ϻt钞0i$L c]ŋS\e)ܹs8l0j{v߯[EOm۶LhYbn@ve?}gm/_e4འJ2l (e,R|M̓yVLLn׬Yc?u:"rO?<7"""'R9̃%III!5ʕ+mn:J?qm}4;~}VL zޱy,ADH񡶙zfMhW +~'77, ~mncǎQxq6ũ-H]>l?uڵ:[d)--]vmޑ9KY&Nh8̃%?>ŋJǏ#B>mvQMO?}A;7rHkhzImy\rHԸS[g^%S:u*7MIIS̰pBYٯܹsϸ Yaaa:iɒ%`?۷Vo<7t^^=&<{[l1)qFo3BCCeD Uw֣-ZR_Ͱy͛KO?zK  ,,QdSO=en$澶i8gBr2v]2p@-'|Bs.Q옙+H yĸTcjuqdܞ\^=N_˃'-7E/qqqɃ7M}V'NZ-;vzjӦMc4hkGsL'&&^62dJeںJ> ~W]K>ճ -Kms`qimy &d2LΝOYݥqmѢL?~gP\bEm7VٯnZ]$bժUZɝ;wR[qjgf^}U9woCSsWi,[qm6XܙAyںd֭X~T htt4#;~0}aC/ޠAeʕ6fzA.]P[0(50l6Wߋ<^^)SP^Wk+QZ<6l:iiiznݺQl2: U-Z)wOmsGRZjO2F6zR3+"##Ι3[hjB: СCFY^>""ښàv< Xbv>P쯺M6O<$xj ̜9|u먒Kaa7Qj~g >Aj몍7.dž 7nhҤ̀6>>>RPt6Ս7֓׌I6;~ڇKý>󘮢k׮^LlѢEϟ?OmV JvM47q۞+a u_~~aS`$[cj g nPfMY˖-ʞlY忑9XnA*Pqכ8voQ駟6Ղ$$$={6>>^~+WN1x`+9ϝ;Wti=Oj_~}zи̷СC I͚5dRߝcR[#  j,^U!gP^j+kC=M~g\oflmjL> ˗/驷k׮!5nJ;aӃߥf͚Em/[ RF Ǻu~ᇬt3<792cƌƍK`+W[n: ͥSNR(ܹUm69;Jf}Kx)RHF>sWS(ɎbXX󩭹6m_E_umd_Z+Wȶ^z^^^AAA{>p#_M6Q,Sj> -w6[۶mَ;f?͛7cyϞ=Lr^zy^>>9ydtշɃ!!!իW߹s?Ky%+V$2)RDB_\\<ƍy*U0̙=h ,v2fnժ46iDI#"">G˗kq뵓m̃\~駔mj[K%ٿ_S"a|||zq=y4E0ѣ3Ν䘘!C|){nx…KŤ]1sjjC=$E-Z%Wy[O}'I.Iu1ZtR 6LJJ\2y'w%P+9˃_|ӥzxxWyP痔wUv풩xҴE\2g|'5t8'ǏY\j\zuK ra/]]U?3mYtܭPM*!eSO>-:tY7g(pݦMCi@jFY``LoFҎɌ̙3:5!8P~$Ê+4kYA wի'nymD>>?/apС2o߾:)y)tn &ç8d4ILڿTI|衇&'ONg;gN%K ,h,xĵjܸӥKڶm[eڵzb1SO=%8WQ,X 뫍W^%h;e˖{c4{"7N&Ir[n߷iӦР͛5͜93Od#֪UȼٌMoM<'>>h߽<(iEZʗ/o?޽{]ʃ:TCԩ'!g9ː!Cr###S at_T"mWʐ 8a///cǎh\I-o?=@ݫ2e?W\3wbT3gδ_|d@777[fM#%WVxْFj3K ,<+42d'$dElll׮]%-[O>'OQF^իgtJ3<#_M~A5&~î裏ʻ NRj.]̚5K n3kۮ] y@AA!7] endstream endobj 1214 0 obj << /Type /XObject /Subtype /Image /Width 1200 /Height 900 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 1069 /Filter /FlateDecode >> stream x1 i a; endstream endobj 1217 0 obj << /Length 468 /Filter /FlateDecode >> stream xڝSMs W0SǶ;ͭnNDȒ+igxﱋ_UmrBP=eɤɡjaG*OM.؜vp4Uv8m=pdHs|O/Cj4jh[@~ =%0_v=Jdȗ~3n_qrY 3LJiV1إ l7W0+ERQ2D(kđ4&DOζz>&zR rntZvBRn}oɍdkmKqE34N1x{(ڏMsO嫣9wTz߈a1R!RW, }Dig+R 3BCʹ[tݐE!ΕE"voXI 2)'V|zT痝4i/]! fo^8fq[ endstream endobj 1230 0 obj << /Length 1070 /Filter /FlateDecode >> stream xڽVYo6~_G ҼEkk}@һul%9 g(r]_O"G3A ߿'QҔ`*{ ?A[fԪP:95r,)]!a)6"eDlΓ7T&ǯey>;M\(6`.52-bqu'lC%5\H%RcgtJIfv6^Pѝ\r51 F+fw Q9KLfN$ɪ]Xed^0 B,17up=hΧd5 Wdcץ$U,@\е]1R*ϏER- .\_t$Iʶ "a;݂@T Q;y@aP(8V4xV*̸|X ξBۤ$2lrp/w̓z7MICS@X˱zsOiYx OrӺmQo(Uw][U?!)6Bi Ty(TR<i~]na 2b~puؗQUh7ʃJnacLcN֣om1]IV/סP>tmgա ڪqTaTgQ%7ZS$7JQ$;q6G< REbuuBB!fw֟0xMO[mn4\52JL^BV%{I Z(RiA7@p;)S#ܚ/N:̰ON]'JH\H2xCtc`{`zSW_s!F}J9!*,ھ/]\hĐG.X/LA RRadP \T~41{E,N3?L\u]8/WVAͧ.>"HW u .o(jeE ,zܻ cW؜:גCxA'}ՃeXI(lC^45s e endstream endobj 1132 0 obj << /Type /ObjStm /N 100 /First 961 /Length 2307 /Filter /FlateDecode >> stream xZێ7}cdK0/c AAky=V<%;~oSuԺbѨ+w1NRC \̡\.}0c<âLE!f*V&+P$q(eY8*O6w س'{ebR[d ZP˒YQvdʙ(cOf:+Q,rQ9W10 FV@?J61+` F }$'Pi$+mHndz"5׋ᮍ&3 %738XIёHΏW գwի귗/w&U5jj}2VNY ZN*x9$:CmNr=%ۓKۢŠP@[#bEFzUīm`6gld󶙎g瞫NsvIʋ4qppu;ucyU=yU=? /ꅚP@恧9aiF&ƽu;es{2ho89Mp(u|w |fj&|WXҖcj5's. 1ёrAx4f]jQyS^ʗw\Wr`2*s%Zy!罫up{Z:8e|8*-` Zz,,kt4X-ӀeR|c:elXZ;$a cQI Eg,Xblx߉uNX>8gÞ@>C%r뱖gvB'ѻn>஀ww|pT a z2Us/989߻Jtѵۛmnu:c5[9R: l>Y}LZRg455'%L/w@I-\-#^Zws¢R~$dKz#o# UߓptY}Oh>#,.֣ͨzRգNkfz=mng_=YY˧EKUO> stream xڍn0 Ew}G JsMV[p;M_J:.ȫ{@^jU=PCšG ^Ty|n]GQތsTtJ͐CGb.LdA۱`Ձ>|7FhTVjR-1&ݤ.ͧ0|̕ GQY2޷iQ3GP endstream endobj 1247 0 obj << /Length 626 /Filter /FlateDecode >> stream xڕTKs0W(D)Ph gzh{pvq\2{Vk}+V{A2đrF4h^&w -@ޠCgU"eI%dA 7 l13$l<%eʈ% Gɧ8&B-Ro/IV"yUvF'$ _$ Ī4\taPe낤K|Ϥjk?Bo*ZxѮ7Y粨x:qF|_,z*xl661G?#j3y^j^բQ6૜A _EautrCHRMdAĈ 3CbxSbyzX{:ne؊j]y nB ~%ʳ>.c*3ʺ)K)olA@zJ` u~}z3TZX3ZwIg5:!\D?\ )p#w7;K[]Eu GVQgIiBDXa^`"+ !m0xo&hvyJ D!~05YZ8xRdzH/0 endstream endobj 1252 0 obj << /Length 222 /Filter /FlateDecode >> stream xڍN1 E|Dbɳ-m[m1 bj-((,W=&]Gvy_+_)pڣ5< gzQ)42YOiH͒ #]ܰ>̧dA;펠pA[Ose4*o?IB)UuVT4k! _sBPTV~%}$~RxA37 EP endstream endobj 1256 0 obj << /Length 2136 /Filter /FlateDecode >> stream xko/kO>&M RqW%s /$dIWe<8x{ #H " pFIf*a c]| A> A1&APDx"F}v{pxH `BJ[7ҋiPD(bvNc $ q{؛ۛqnØ 6L1A ==6-p1j0V 18I g"YD MY]C#e~^s|te_Taǥ3qH|氆?ԡRŵ88j:+2K|MbBwu,MYڢ!'2C܀ ϡGQ$߾ȠLGOzMxj HހZg!cLk$w[iϘ-&JRS j!W0Gl2Ow պ!WVi/Y<,UVtLa7Ŭ-.aHJ.K@uzˆv[qKS/@^ N!R]Td6=wd=9'$.VKv/Lt$1O]9x0;hTeG6,r Anu>n YÂn}^%!" 5[W@'ܝgB^s$3y2 PHizH?Gj7yb,䵉=U_CЉsЦ3R\_yH cGݟ$8/8OcqRb]gK4$aT7 mH>{(9:<+W-Y\}޳Y?NJ;;:q>E 3PA|Lw.{{shwTD4i>\ThĥK$A;kl%|X.m]%5D:O80%e/@ ~eDMaX&QʏMPOM7^nb%}eMj,J:W5 {b9<^(q3ޔÊ*-@hYJ'(=Ijˇ`fl5kU' FZVGZu;9߸g0-jmikТ}1f9lXB| N[}JNeKp0C(`~懬Q?O[dU:ysD_oŎh A:j;WPo\hE 7wR{RuV)}X=ӕ[Jyo-5zܣʕZϜg8k(]|E(MB!O !vҜ(zrN"4BqȮNק95<"IWGԧ2&# -!иګǝ_H߃R\CsB́84;UHa|^䡻.Feȭ7B;{ AO3d C/Pa!T2qInN endstream endobj 1263 0 obj << /Length 3171 /Filter /FlateDecode >> stream xko~š_"j\4AF @lF[Xqò}gvfy\mKDwbsg$^77\< e>Ϣ_6C 3^hGp# a(c`fJ a(<]] :ov4]PmW[HdJfoQ*}yU GCBIV ݴ;mTrel Ӯ Y gʏkc]TmWvΕ)z4K \opq$~׿BMFlKhBB?&VO3W*Er7a saeAxU+7!Z~}M^k;#;^&|7 fّDB_h>SA,6⏳(1C 58@ ZϜR^H9qD< f-<1#c?%(V^-~V 'A' /o!\7kQ82 L(!pq_WesK뮭m4L#q3pZ f26 f̈n)X4涧y˶0M紦I&CG&Ne}6zbX,;E9ex'=(h+""t^[ &ny.+9fs"1MSڙ($|Uij+lC@xesz '΂%gW u\r%MUM ʸ#M%);HfF KFS є1łS?Cg̪2H||#pr|EƲɋ-;EHeJTy? ygl n5!6>*<ȁ_.1d&:0.\X{rXSNdjg۵ [Q!eOL57лm0cZB; G 9=we3,9@9lFj+h46ZCCŬ3t-w2bҐ?,KXΈHyENށ;aw]Qh燼5fUYI@lIH"$6N3WFDaF^;o\@6# z0aJs 4""PnI!ZH0R֓`^4c-^;3+gPV,o![]6[s↼# s_9=$%|=J~mFP ɲ6<: ڈɘ\5/Ěs#ݟ[TGflyJ/Fc@4{8A9腸nL,!~fMæ>84Nb}ch`1‘,y||N1A:Te(lNbRn,A?{Ļ9C]ȫBltJ9QV]^f?`)(QP,I&s&`Iyڊ_^WmoC7/؃3M8SLOl`} DI4, `#I8Z :XWǛ P;o+?9pUX^>5b@rXB`R75ouv[vO O}'S wT̲a/T&e܇~wq۵e;"W q!@ H U"W#yec Q1es{zP6gS'_˴ h7s)YS}_=L{#1cAm{,b1ŋD /f~Sӱ js@ձw3wpbP{ Ue dp.2=-OE8)xRԸ{˧ҷl Saݧ^rqTENpViG=҆O¦J]"mqfSr- ?~& N7r&RW#o^pX)*)AOUPa. z"$[4FPuǴ `d8vcM[B173ҷk|0pO=519ʼnoX jH]vxQuXXRbJ[*,x{clhhw/ }`?Юyexz4zXQW%fǚ~"95k(1cs>XjaЛs07HY3GuIKUs 5 < ]^ H(#R_nQ ;n 3 3[cWfPl&,x|0$O$Ζmϱ4F'O|tڦ^O]ɵK };vqjM,㔨8Eް̭P^7j@7ms ٝW_P4<889h0x6Pܞ㤥O]?s,j}#T|AWTh8xj;[+_Wߞ*O#TQ?Bz:kkJe= l{ endstream endobj 1268 0 obj << /Length 2577 /Filter /FlateDecode >> stream xZoF (u8A. iK] 0hPG߇ofgIR0`,ŝK;Oyv2;U҉XvΖx^ȼH9g gln䗓 BbެL}7"_'YW },d|w'N~=wH`O~; pй2֎C̜Ou­/=CUN 8* JAf2a]z{(q1 Ҽ.)zGtm٪aS%1x-v~1|u5`꼦 VJ~Ï4(&w㿤\%b=3vMsATW%/qMRvT҃`^UR ފ4ryST,R ;/leu!]7kˆ%ъM-W^5ZANc}UI]] u536/aEl2UJ0caEOМ&z%Oth9{)DhY@b""e,!wYt}D_cH9 l>C dҏ L{(|s^_I1twFS3΄>NS(ۅb(&y`yV|Sy痿i]}]VGf+HHnzY4"(e껖Ww\[kMpϖ)&( bXxb/(a10# 88t )OK@~:~vZ1V(Ĩ C v-ǡ`mfYPE [f,CɺTI (zi>/Ž}ޤ3[Kaǭ狩ckӹa"î5}Xg”%1XZV AocSGҵGn#㩒(H*VBL~m?pq%ou:}OXZlmoWy[>GD1wcL`z~IrheF!J8%pĒ wMeFI9) wNv 1 zѾ6@eE^,סS) C~{\E\>1$BM0Q,n9cE)r07.U<8M(t/b #;֞l ^iư *<@i@-:k",Y J)}-R9o^.I+%}C.4_HYbhyX:Hr+Dr*K_~zcDt`)/RG֍b M{s}G'P2ݝ( RZ;SZ?Hbd-Z&6IUȏ_v} ^9#pbU~v@_Y{P}tj7~YM?!ȅiٌ͓P^ޖTةo/p%T[e Й(NTI֔aLEuvI*m~P4"ѡIӧ3!Qә-pijFn+>Wx+UsQՇ.hih5pG 7?>oYN%yܺUY(dr|Jbh [/:@9G.KG<φ]2G[o{7%];#p8'?pFcbf*rCv~8:?C'?K'm_nm3sS#~ ><槧Gw\+E09 C櫓[4osWW^HIq71ݟ O2!o=6j/Ÿ{ A\fc$ endstream endobj 1274 0 obj << /Length 2330 /Filter /FlateDecode >> stream x[oF}D}?>p>]S #m_3\Riٱ€\rgggf3*g^'x\'ቑH'EbjԤW7v|E=IZgesV>0b q.ޟ1&,P̗gAP"Myge6ʫ$4^},g(Hሶ61MXTbr u\Mnզj{+05KzB`Oob A/mtKX}R[IA`LÏT G׭LSDjdA&lɗGޮ#RE @YQhia܋/g U>n@JЧ$pS LAtү}Z i 360 %a?H`Dŀ{4 ?G:0 `0Ra@2IԼEH-,E!~,hĔN~N뀠Yu SF'rٽP joΐi2̏U. -W'C R~ޓx뻞-:ھSmKc@JU4_b\*3KPڃG$7/0@QtF߳9._nySyWY+rqB&.Z{Ff uTF#Ѩe4`d#:&c>!Hi&&Ep7ѐɳ  ь[@ƀO  -9y|A)6z0F&Al=Vϔx`Z!و,ɿe(Ƴn56~cFl4TDV1Cg'9rذH!.3֫:ͷVY1YyH f[[wR߭#ht;M*2rJYeUTZ; ӓyZ`L~l(VM֛z&>K~?GbA?cɷ:< ;/+۩&kAʢ* vaLs oFKe=i/<..yOV ˴YKKiih5 mf7r:c}㡸+.A;m QsӍ!v"שJ| ˝. \͢[eud> UlSn}ݾPܷV\@ضg*l]mWv~ZC]"nvESM;I0yճ῁( rT$'n)Lq]kp\j 8N@T`Ծ)Cx z!p3g' endstream endobj 1279 0 obj << /Length 2531 /Filter /FlateDecode >> stream xks6/_*M-$ݤur\ڞ\dh 9|ur @TgɌ v"G/N^0jyTXs رEǬ˙u5;|b<;>"Hc=.CR"<~{oGaX6r4:z`a<׺VE-\?Wϲ/3zqfEYxbq"ƅm!q-9$Ik4h_+ ֐!އJa lVm=4`q^FzAS$崐c Vc0#SkFc"]< Ye n7@0a F5E=!cח|А9PP5λTfS8A(QnN P%j+wLL4Ҳ0-n`E gIT>20Aʪm3 &Zv2[Pa` 1#TmXWPɘѽ~%O4?-et-3=N2e*2\j"jG3(l QR'!|T @b̳\π9NFi4x5^ W-Tq+3PW=n\Cd*Y%IW}M"Z<: 6EW@[}"m>8RO {2q6\nX?]n |L@I llt4,XblYhhࡲ\?9Œ A[ F x9‘ ۟YY A3c TH*:5:,`>({MJV+Z8y(TzCNlyK~xm*f/SͤpfM^GrHw]9 ?6ȩ\ ]:Ehn]ʹMzq-IOՇL_<? ן~9 J!%A ~DZT9ct:Y/}ׯF2L ?<~ا\*O˳ {|h; L`iEQNW7KqAh7RVKZfS:c[vL⠊  >\K}9H7dN/}N r9v9l/p[/ovՏ'־ Zٗ[2؈J Dx祎g_F}H;O]WpUՇLN?8u""*7׼qN`Y>/͠o<j2P~z]Me !FQ8}a.[1 }eK#(Lˎ sA>M?yjJ-[.:1VoI\qYUwpbiGQ[ɕHjۈ4@aNH/4{϶&v_צ# endstream endobj 1284 0 obj << /Length 1468 /Filter /FlateDecode >> stream xY[S6~ϯМ'{JIdtz9tM8 A<8v%P{W`'ffVO߮Aǃƃ'I4D>C>0=4Sg)2?p~.JKw(OTBLܳf@a)UͱO|t14gD֌#.x&$/Bqr1`!q7 @>%X2+"K8Ĺ̳m-r2,*jIt% Qm( e T{@˹CBmUkO`EMI5*CXw|SoA\g Nx^kg>BKu^/Y?|2/li1+^RC!wi(!һNG3V#m.aR/_##8dY~(~>gfn;6f5y n(ut=瘑X’F@y}tzː#AXml_ŏs̀`m&Q=.يÜ) %Mx콌_3֦(>x^%QBⰂ>fZ'*>BK аѤ 3N {0m< )1^όlA=w Utʙa%kAh,'dԋJ˄@)lZQd_3t(ھurd}7Zi|Z@5V:5\8K׳$vqVQHt ဘ超6zv4iV3$( A4HB*@Ј#ɲmiPEg_4mPruV¥@KumK#Ydz(h]xY%ETiMu1CδM cp$8XOWC,Q[ ï\y>P0*(<=lT$bP&,uƛ%ٍ/Z9 <O%'O-dP}ZA|0$*nkΡ^/U{wo˷8\ i6a1; ڨ39NA }¤+l!&þX==jI un.N<\6=}tsy6&qohE _uJu7rx}C$bm{Yf˨c+U>;{ƊRgex{RI8iS4*TY9O:cfY6fq, d,)6F5:xWuVUg3n^ɷWsז}tWB0襽-l%8f?u%3=e~E Wguv'}+dՍ_Lk}[d~/[Eon/*v endstream endobj 1289 0 obj << /Length 829 /Filter /FlateDecode >> stream xV[O0~c"/iMB1%"$vJSDQ|9&h;Q (d(<@w~APDphU"/`,Ykè7 8XFȋ)EA0%>e}(aLǘ>8HɱKF]?!6pŜq (Ǟ7s2rblOKPR5OԜLjYJ5j[VO}zEhYO6ˡ@Q*DB751*EMe~RUvs.@j9$Klv7\.p[kM<ۧHژkr6t[Se6LN]7u,{IjaVMc&31md2-JmLZ"fL,baצc3þ1kp#nU$ endstream endobj 1296 0 obj << /Length 1105 /Filter /FlateDecode >> stream xڽXYoF~ׯ# DxE:*P pHJ}gvE+2}ўkEGf?/gsƋ˄Tx̻Am jR릩Ule./۳8,yzԛ!5`Z8R mH;7"Sp] B+NpzHIboZ]ݼ\,~'@֞gNJUI^캲8!i;P-QJ-9ɰ=p"/N[r )Y1 @Ju3$3۷=9I<⦜%SGF0b#Э]ZM-oTΝ#$)\UHLB2N{u 3@⮲CLf܂`G)L.btq:BW">@K"ah-$F^UvyF:/m]ӣg0Yu0/|!moўW}X#SpVmN.G_PA.m N r<M &dx #X(=RHD {@x<[MxCiSzٿ@ӛlC#{o;J*8YT2J^=29 ռuծcū)csJ(^KINMSb{:^{/rx صmK8fqU⦩QjTyxxkpJԽo\ ntcUw69%v-*Oυ.L}co0ri7a~-4D$OL#;|#d]ћ+ oژ@QnΩTo~£'?OC 98'" 2/;u`'כ`؏06t}X .K endstream endobj 1300 0 obj << /Length 418 /Filter /FlateDecode >> stream xڭMO0s&V +q[6 ИRiB*{fq_٬.JTN9@B,009ya]\%/;ynY3 >-(:LR? Fbe[׀BW%L:0q$(1I)xNS^ləBI<̇Ї+d\}E䴎!-]\}\*5-b?X 5ݟzYҢ: Q|C^Wtɴhd&rJliޙ|*: TҔ4|!Hd,ѝ>k/+F )?T 5+75 endstream endobj 1305 0 obj << /Length 223 /Filter /FlateDecode >> stream xڍ=o0wmgzC )% ߯aԡu{' ذRGoV/!aǃI:m&QT}[lb=|>l* i-Y8l'8&} w=OKwQyU՟Z*:sK dӻY}Dŧtp( =_ C?>b̍> stream xYYo8~"/1P$uAn-6UvA@KL:H"%K콐 H1p曓$" k,-ֈP!YBaynf4vMoA5y65A.&QDvn[~OΧWf 1aڴ^æi:5GHƟlNuȭm91]4m`lb s/S`k7!GZ9?f?i; =ADCmMw\t-dZsm'GSm#mlб,TJRLDJ&;de!LG4y M XRQ l/fPknk7k>/" 1!:D`2xgkj`b ]M%|일=VkWPyRQBϸX/O{3ـ0aCW/ʸTLMIf0*P1'}5OkvW x.QXSr7dedd]ZɒPa(Sէ8l_/p8Ţ+;FXxzYUa M?n \u)rmD 瓬V?`.GH+.Ɏl@":;9td1_erQq~8<58{=r恋,deW% @yQ%宺~AyJX܌"%Ъ㜋`bpPo&/Pmmsv+|" q'LENJaHj6ʤBRg.I/y#yD[XP9kDWO k7ܪ{"lWei=@R.ب4Uwlq?@QUu_N8AEEnw#w^-K*Y",́pFo,DPH=x=|^%tb}EEw@O2 D\6l²- Ea:&r{G0 s4y&; c pZkk8WƢWO  ʃiu:WXDvg%ȣT3)"lm† endstream endobj 1313 0 obj << /Length 1969 /Filter /FlateDecode >> stream xY[o6~ϯА%A-K}(:IeXYamsDf~")يڻȃ";a^LdDf#ߘ̍nĸ=IY “ ɫӁw+'DcixrrȆ,ÖKf`FݾhXƪ35nYJL/ 9ni@Q`B׉L? LR,ϊY^Ѽ& Y's29*89EeuxC3!l} D\x (֤rQ(<]nLyv2sڱbTm ȲP ɗ8趕_BK'z1HmJ?{6/̬lIzr>f3T-ip.)Dلsƿ- ?i+*tNnZ&ˇ1{{_m?a6@Kg (s hTL.M,1pKw]:{⸪qj)Xw,Ub^xi%ZiʰR2ys"Ŋ/afϞ*=vd[ն\ӷ)UC]:OA8}4}ٲUf H0Sa:::9Wu-*<׽FclWosZqբ ذ9>Jv3tԞXs' -}GoAb^ wIlu9ѧ tֶvƒ'ѵREf8- i{Vk@©l wK|7&2 ~&T.O|=vNɼΛ$/ . Ȩ;\yds+ۻ&"iYTG'SHbN{^OF["rFG=ܲq䁿ye5ӕ6)0a!}h͓9Kvg"@:MJ@ d]4iڝ=ٵlV)`v_jCYW2I{5nT MmKzwP)pAtCΛєVi@=j(8Lu56d2jt[J<.@Fsՙ!;W4Qv%i8T}KgJ'4C]> stream xYio_n>4Ɏ92973=h[IR+DǢ_"L{>E0gfg/8ȷ}c4?@"7rYb|]‹h\礨.'RZIF0#Bd^~}8{=;vf?Ӱ. E}j <`ȉBݕgfL~>3f[2ŽEi5ZN04Dp-nzfL*-jy,iW\cJk&UkVǮ}2ޏi"nc8 xh oc z;=UGrU'˹_/F-8,yj(_ItӼ埌T|D+V4ِ'|%$-AI6Z KZ 8 eA2-*Rv*lIVX2cna>Y?2X"fHu\Gcڒ$Z ` E4x!*Fy]NoLn(G > B? %i4螖;I'M T]`UUz X@ -Ή = W+>n!}lE$e)ς5K,%.VBJzB\> K9w|qny7ݐ8] NCFqrtN~]|~}|#t,z1'OwrB4<[qiA'x+cdEoOiG\68fgdXH5^Kuz1iEY^)y⫦JS e]0Ůb6P-.|$"tM`boTyz#@Wt+k̹v&r!'Φ?M5тm*\k"3VvtdI/" e)-9e>WW:kswVA鿵䶺ql<*(d_2Z _4I]iJe -9q%eW2L sQ]049 ?\zbR2#([ey_Bξ¬j/긌vW?v dH5l 7oYB'Hb5=eqxZ}shŎA9׆:v :jDMhyۗ?fL rQHA]ִ|^ժIY6>OeH7jTNKxKϯo~qW`n`ACf ׼/-qwc^tzttY8aU!zrL9:4F3Vc'`Q.rڃ TS R ϛB} G (a#QY;ĄӀߝݴ=qڀ2C@YӐwԫz9KmDʄ(ޭ̾k)S𫩎B\84@  . O]Ҁ~wi>g*M\5Q|B;Ŷ:MoBEmg(mõ^nAw,I endstream endobj 1321 0 obj << /Length 2068 /Filter /FlateDecode >> stream xY[s6~ky,Kfۙ&mtnbӇlĚ$hE"t1N!Vmtf~Z JB7K+rȏx<>_'Qpvj2uv%d\`Jߐ=2ÙlG(#+-> l%د*-?[XOgd׬,1IM%p|/Aa[cЍ;˫dd`_,ZQݐ\|i39 81883>L>W#-`XK{74Xb!Lna/ⷜ~K3"FYYWҴ`].ŷkI#F%:,j \eqP lVdSq8UAFK֘[H2X{r!ra0$[CC"J"P!{>ʺ~Hd̺Er1NJ21x}8PH-؄W߉cMc1r+R)bo GvM6LFy_C)W5mck"e^iqC{8 #}䨉ڽSZӖU;~nl௲{U$=Wc#Hsм7*v"BbRah+1jH]To(æYΫ0x^rR n*˹&ڃ孄La=I8/ڡ l /(`ehDC4Kb< >ʛ&3' "M~)gZttF\7i6oqwo}&ȳܬ ޗV=ddiA[U&mG^UR5T?b ^5f΅J㞹sYB쳡GP|'}ō#xSg#m(Vnj6%aw-(i^/xX-WQ;ߥk'4WNFEݭ4o ~FZ]b)5;5(. uuBȞV :u_ H4OCuX`*D<‡O_Z"XWVW"hV˼)[C{@+2NU}thb`iJ5ѺDF_৿)?2$zb)= M@EGLJUZwnFQޯv[hN3>%Y3W]r8Ftbtq\+#y̓~zG|>t>vvݪOsGsJ ]N\eR̗W*7b?сuOFZү#$`aNЅޑMJjvdwSζK|j *!2.^݊Ў&Ht1*E ذ/H|nu{k kt:Qe!˥ܽA #ນGl7Nm[#e BhH^M !ojSLk S¯ʹ }téCg(__el*:v0jRTY6PfDxQ8R od߻w ^ 'ΙW6Nr?mGqۗ6 I%o sˆ-z hSKgJY^}+Cjt 3zt EnL=cEFIP'{uRj \40RICJ*FY{ xrsܟ8!#;Bar ?Qqyn#N4`:F1ۄ+2IgMh1Sp[^Eg=aŶvk\O h&zϻ.LC?3 Rnr endstream endobj 1237 0 obj << /Type /ObjStm /N 100 /First 953 /Length 1882 /Filter /FlateDecode >> stream xYmoG_{6(B JVB@ؗ^}Y >j%fgg晙#2(W>oPVr'+GDʇ,wYV!NQr#c'x" 92Ta,^Rvp"y9fl ̄D;+{TEJR.`Y>5(ZB@`;g8yfAYH& i SDR.;EeZ#E89 ")a"[hiG}")p@rC/K p[5P]v!82n=.JB@B W l9ZE>` yX2' BURE@D(HV#P*FJ?cEF0Qd#b( X(z=P "b`al U{:5| c + (Q $#/NH0&CPNyĀ9/q EòE+ yAϋ[e/TR|G  )1zpF?W՟ Jp]=xp#SNZ\/%M)lIV4:8P{Y锬\^O z6B'zY\/_~`US=[̥y=o.I=_rlI*f [F.N(* nAܴe' ٬eI}JI"|!)KFՋElnT=lړ-ۙWё-j"C7NRA;,y__<=mu+Rkg䓲_vvjѨM(V(}˕1ŖPNgͬzQWbqqNc5uuwr&lyk6/ӳɻq}zN~Vtb#v񦺱Δ`R{p.2óf POtWoBw і/j:<.9eOJ̛Co:H*R"fyDۦ=eV9ڡX71vK5I[{cܽZu}@ՖmmNHf-m?|]z #niv:5F4پJ\k}rf5Бg F1j M3hHR$S/4#e0hC\sIn+cpYg 5++06n8&^Yq/;]zS'+N>m1%neti^+ TvYn!c??m{d91!q#w0ec;wzqo>^mhkzS߰HٞN3 ܯ,\Gx%b:?>ʙrBD,'89M]淓f2&XP]vUIUu6i=#un;D{11[-[)h.3ZF6mg ]W=x_']:+q~Zj {~m7X|Yc W H69Ǟ" ;,{XгgXe쀷WoTg{''qkޛ]p gt߹.p.wI endstream endobj 1326 0 obj << /Length 1693 /Filter /FlateDecode >> stream xڽX[o6~Ї%X͈ai[дi%Zf#*I~$Rhl( ;i[e[&fc(-,7gVEȋ]kYf&2?:atK)?K$en#)H"HQ/ɋ>Vh"]M> ,qdۯVD,ɇlxnuZ/ QڸscDbNy)+%-k}lՍ*N잲Z$of,í`L7xB ,0X {WD5ݨsu ?n?:8ҽIJ#MWy;~0 *R)X$J(KntOnPq*ǔ:Pۄ4yqmA 4ACB8g(32FĘppAlx(HDV&BxGHf 1 Ŏu ك* &pGmoBSKQFlFz BO zOPY D(ffE[!]`˔eT *&ZszBxv[RZ^pѫsvG=<p(F A'-8A?Tw yH4-DΤ*N*Sk4s]^l"'O-rZ3FLW2Uԥ#ԅ%X>c djpm]U+u)H9mUKX wꂜ`(Zy $uU%HOGt0 \8O7~='(=ytԗ*l.ի!i-~\Ξz lǠ/#Q\%B5P\Zޠdn&8_ 阡[I 2YT5K ɼ COr"b̈́v1kj|)FD7ylaž[V8xuߎ@{d\  H8od4ֲqa.?/% #ؙxj{Ým64G;g|6ȠU]jpWE`]/U}<%ۥ,9+ $yG;P`H06* zؚU[в2ӼgecW1Bv]cw{.UfQ Cݕ##-qa$V C5ϒye"܇,8|w";9C[c<v endstream endobj 1331 0 obj << /Length 2137 /Filter /FlateDecode >> stream xYYܶ~_@JAQ,ey\q1$C/Ipg׿>M s$} C>a[e[_cuZ IB7V+rȏx*߮Qp+:_,('Oۊr n,xbJFqHLŇ77_n>8m9>Jl+o,xIl*cx7v[[ݗnbE$l;%$c+rlpNy-eطWhװ>j Ϝ8V3aBl;t~f}켡77X᨟};'pA,`6ʛa-S֙X 3Ά7#SӰ (dZ:I7W73|sj0~%mXwø3^6E+LϞڈpDJZhej.ݶ=F 15TYL`AQaPN/P}ZoAq! `Ӻo2_[Kq#Hy[CH9z!bmn1,٘>HH0Sr# w D&>xgZ H# [ZOmj1s)tL/Wle}G\khn}  1EUC| oױkKHGҌ$#ݘ0 cVCEZPlYV <+ ,HiMWXp`[Ǽ)нe2pOX˼c>MyRRhZ̕{3Qq Xgk*"ÎS芪\EFo´;< a: Bŧr:C9w <%ÓNgU{%EuHEp1@# !qޛB`*xsFϲȷ jm &^6 (1f ˱GOA!Ь*؄kq*mD'߳Kcu5( B#-[M[2lFe2#cqƢRWc:i8*x*{iP+E<۔ `S$<>]hD/mk$GLL1skn ("U^?6n]>.[1RR# ],dal</sn@];YKdzI$gUfH,iw{Nc> stream xZmo8_wɢ߲M ܡWhDD%J$-rh1Egy8m-w?z$tCk0BY#?Ef}\\EeӖ뫙ŗoXWsʗ}ܐȾg[0"X"-.>} l%Ѝ*,?[.l)ݽV~M%]^8"F)+9-[}Yѷ\=em#{Zq(c7&{Fd;/ c '|Mp~Co`5j~30 i1x3s5t9bEs۲@\ gNŋ2lpV s 7Eⷭ*Vq /JQKS.Co5H%m[3ZV:v@X=33q[ 4]#OŅ/,KE2ǫVu}^p yPֵw+Ȑjn8[躭^)#!5[B=eEf2N!:˞?G48"7oǹo^r#ZHPpJ3u@?-^xDRi۵mxM힫a)ɬ9*'jXv(K)6%x_[IIj"ȁHGj?Ҏ䛩y?d»q0)㈕߮ڧ=Yu M#@yݍ:gGhAA%fxĭ_a0yr?Qpyn)p8Ќ<yN]\<y#?݌o p!pc+w'(@Ò;G$ endstream endobj 1339 0 obj << /Length 2190 /Filter /FlateDecode >> stream xZ[6~_<4_E6mP46 d-6;L&~-h}X9<| ۈG5%I8Frnߒ0.*ٞdQ<';Ɵ9nkTʿf{ڕRFiC:T*K*.HsN穆iڙ()LE[<ר"R#*C+60OQ˖e'dV(Qۦ|O;5 mTqQ`MrdTڀ%awbO6}\K^ii=o~2SuHН24r D6S+ ¬`4sXpYFNG nƄU6:jdD%iHcn0 nBb N[J̈<<0JON*,ݩr"Tfrm bu 51b:klkn@!Nh0 Ux;C*G%)G%ڐRgK,oTT+)c |VhMm{ LWB :TT€>#7*0l1 =(M+vdhׁ'm"G1m e:*p MBBf.`BAYO,3 UiOMÔ{z3|_λ_K9#97w{u zzJ*blIe?:ee04KԦ hʢQ >KˮǠݔs%lbJoɦEDtOXj ta~Mw V] MxBMQ=h%pyy(u%T|`)Û97(4mnG '|jQt<@Rd@5<@6/5<xb='pD]7v;Ӧۉ#3CC-S5m K-0ɹ#vt]DF|.OdCK` ^K>*[DJf.,vOf/lVo~,CuQarFxar֮hq(6:liQEfjOŬ#wxQ ]~c.yb0 5: Śs޵}Xľ"1hzT}M(D K/)+T,^'J+v]RA%ߊ#=Ty:0̜/7vNፅC\8Qrs":Z5xt9- TP|K Tt6`ɳ=9'dؠtV/r}J,84`g-o NT oZ S!~j 5*CミƵh:jZU.2C7](*Dc >}2#V9d7ӴihN1f]7{Y_xs-}lldW O/mAASя%Q%06cT{da4(}>7cGzR9ݧu1cB?9hd\΅TIl+THYMǛTZ܌PՔ"qצ׸$BFɅEuC] .߬F^y^\1Yw?PeIiF9q3bĔi@Fsz(py.}D'ni5@ 3q'ή&$W>^#s?zg.cNxL' fH -n>ob`ޱG_6YxȺ ՑLYADBʎìgQ+?7X tˏ; endstream endobj 1343 0 obj << /Length 1978 /Filter /FlateDecode >> stream xYkܶBQt7z?Ap[c,8GbV"eJH=8u;1$>Ϲܲ/^\xV Ȋgdև˛(l:_m(;Oj\r|OJ2qP쫏7?_ӅٖcE.LHm+-yIl=oUp-^JV&rkyM%H-|/Aa[cЍ-~;嬥#ؗ; .)5s9B3Pǒ?nVGڇ 0Bח~&>joͼV 9^<3pH2ˤV0qPë釛-3\wu-&&L #MB4-Mw[>\R㲜S8w$g l3|Y*),P#][@дOD7Z8r 7]]s*pIvQ=ǂmI=f|w- `8ڟRa-bfLDݵRz Z\.^-YQuFV.<"yKf-LBa-3J01\d8ٵxcb eZmv*9EXYJ: a7T\pF[.蒎śO<#{AcQ tU;-l+$4f:Ȟqud C ߯Fh M%IKA(9 D5ѩ3 /iD-;m=Gn=UtkpRu4;"oïGIxR(ia ъiuUCsXXMUnaY,Ym0M&8N~WQO?@d5J8FigFeӌx+3#!qK)@"~ّIP\Yp=cI nuRΘw!SPkW\㾚VFSv۵T!ͮccpUdOn Ndeͣ k+x'K١GK[(/$N)z7yo,WM1o|VIh?n,-g}ӿ:yNýQk<M1-{ B#!TY5`}M[QXYa]# 5H=Ty!c_S]w!JB4j0'h<շڻ'n֍Fy_;zLyy-2ξӋPӮiu%[i.>yQS0X=%M-+!5.PfZǪ^j#:ɺ|AX펷Q┋JKpMd]5r5^kH9c}KGo+]ݒEm[BkȺ D VHN(0 FOTNL`/mb۾P8dK liS̴Z]\:#_niI%*==|K օn>Oe+y6un;=5Aziǽ|QcԷ["x3q}BpƎ~m50}ʮϪ@N4@wZv45}D>h4ʌH9;wK$VN e/'>\=<%kA^$X,_t0 endstream endobj 1347 0 obj << /Length 2023 /Filter /FlateDecode >> stream xY[o۸~ϯA=Imҽ'qчnms+*)C"Kthgf(q_|N؏I|' (Lg;'It,j}2ڒT,: eՙ"?#fˑKD(q'+>ݸN?;. ҹsߍ*0ópj5_;q}mO AI<~vZ$%#xYZ5''~t|GY+t^Ӽn xAjL v)P@vIP08+~`8Vgï3߽QRfVP@h0-johA`r`=l f(Zӫv)ڴ0燫wCQ1=㟭5g5ţQZL?kƛ4ݣ)݃~2Y6MS'$<^!18UM,jvomnK(B߉2E߲hx$j P(ChS_x&j-#GOm΋ *]OTL;.kEJ^fZFwe,qy^ .nkh֜`O,B)+@N2QLb-ں_J~aqZ.#|}Z%i6,.k]<# Z?ut ]ܴ\TȔ! zvo>+KKh{sJ: gy YsJiUB 6)qQ [$A xn8dYQ~{WN!3#tX(&( %b-‡@u qIDVPT=պOpS95 sV}|VI&9a4Z # 5..6>%Qo ijp,76WgFt JZ az ƒ45P7|DUeO~*r%x6N7#g+,[ZhUT("6FW 2( sZW`Vm|mK.5t-t&N3AKa=O o1vVLg7ݰCL  n5x-ATi*[ϔ؞)=tYpJ/4!`h3E(=kr}( P=j&$lh^+E {W|L3q0iy[SWP30(\Lu'X6+c`gS8䤊dϻypE,>`ˢ"K*lla <fd%s=P `;[oZΰSIu?/XuT]kZ=WlXg TН%X$f &OSs4#)nYZ@јp<s֚:DZ|᤯/3V&a$ t WhChFvɠbܢ>ըDDpF;(gUiԖd!F;¿ibgWmHHTRkCQ-!{}T,%LlaTk[[o@XfLRw >8#t'6شG?=miA1;r;&8U/v 5}|7@<3P [A30g9Ֆɞ"Rˉo2RtIϘE4{M|mfn:s[oxz;JǖղF<:T[[),Y:e}CSL].o77s4}W7P7( '/e P#'n0 QDiWF?b4%uROHAt> stream xY[o6~ϯև%A~ 0ln(:K0hHGRI_C"KwW`h utx}r:ߟ\ |'CY|qB7q0Ea8p:?KS޵볙h5g+R̉goO^O~;}9C+G)[EA:Y)\kWn֎\}o]9 ד i$b?QՍܜj;R{bQ #g~tzWю;x-xۮcϑL>ΐA2 &8:(\i`덃AT,F^ 5;. śMK۩_sL`~ˆ83c3A|]kjL U#ԃL§d^`'.|a!X섅3ʼn3&4Izx wûPZ&0@> stream xXY6~PA>m hti%Zb+H$%KzM`w!.51D37׶(,O[o5|'@NhkX\|wɛ"Y,_Q^״, ̉D2V֋ 24SvoZ/>~2i@rxfޱD7N+|*!@My/p,jZ4$!]cee.V#+]^Ѳr'NcLi0!r-5@6h (_Llva;OH1o:ٞ˜'"(?ҟRbnڣ(TI5C(!aHE*[/U{f1- =u 9}Jx_T\*U7N׳8>O>~X˘,!_9ޑ. ӌȚאGOA7k,.>r\G  2kb#a,) U:~\F-˽hCjρ-㛂zAe!2dl2܊g1TܪgHS-D|e%?0OM֐>:^P*t]| !yv*% S(R ,-a}J9^lꪑ7.aW~I<;0F<;jrSϱ1Ph#ZsA!> stream xY[4~_1_ ^@ ;HgwfI<8txs7 Z&y0#-~E9Gӯ_d}!7Q: l"n\GymOR_9^K.~~լqЦޕ&E'$xYY 'E׶pj\SM7aWq!nJ3VfJbG 攡u!O e/N6}+j>,V+pPI++~o# oL8n]nE]^`|lR/o6F:Btu;Mdsl G!wm y:>1?a]eRp\lHqiq'.d.f$@-wh&H|O]ژ.[CH]<_AnhAb|e)9ni b5+j:k;S1g=n-+llZֵ6 ^@lfV5,X<)@q_Ԥ] c1qS3>Us׶WZA|ܙ&(!JÙ+S}-%WƧi8#$G \A9p"ɠ-y>Nay> endstream endobj 1364 0 obj << /Length 1750 /Filter /FlateDecode >> stream xXY6~_"5ؾ4צ t A KͮD*G}")[z]CQ45|3XZ JB7k+rȏx2>ϖ(n 7gY֔E0KkڿNV/=|fyV(>ʓ_l+$S1< [iWoZĊPَTƱ96 Hi5 A9+ժx[wҍ1vbr} #$(p}iG[ (Jݤ@6R\9b|c`L@f@G/-i^}ҲUXqV[[&أrNnK):4;Ro5od!RعǓc۱2sbƩnM2&1{`B.;yPsk!l:$̱L@( cCS*̘e@,Ph17=dcݷľA)!#~*H^騬=KHi:2h )#^t2AU6zUSSC,eY?]T}\UaF6f57erW1:A 7LqETjFjDlq-hB~B# h“0_QQ*p4O}DSV-F:]_)sMu\Iiډm flBwd;9Ӌ`Ec׫^ʐ$NQ1aphiGhHdh_[C8HuBb R6E @FRJ~1(6iAi4B<\av-˧i/퉼ㅛחqWNA^>UF㴐x?AwP{G`Zmnouᦃ⻩">hE {',c'G3K^L#  RaFY9r.ػ R]Q%QvV[sVmI-?@v)䚥\*F8$/ɶy2Õ/f> GGc^,9Ekép(vܽc3K੹D{ȑ@!0";Bar(n~: C71gճTI fagɁQۇ܊mn[^1nwl5\ohD5sH.ئ _P endstream endobj 1368 0 obj << /Length 951 /Filter /FlateDecode >> stream xڥMo6b5=&hf'7(\[qؒ+ɶ~GljiX)p村 y~>}84d~G6T!FZ* 5YLrueE3~me F-|ψy@1ђL($'pY6GH\T%@Ub+6D1PR*VRb8mbP$KpžN AFr&jb@YB鳈f&x?JNR#jDBÑ2^FXF[!/{1}K-,F> stream xڽMo1+,+;~EʩM9P0 X^&]WZ3ƏߙbӬ,94i6]2CH˥l`0*ݪjn)&*5j.oj? ui0l #ZrͷE_3Y-چ}˾fp ǹÊ1rpgp\[ dbA5]A|yq?Tk])2[u1uYq@d%W]\[Fbϖ!񗬳8v1/7;A»8o}}Yusj[ueܷM ;MS9RHΐ9h7rϜ_^+c( ncl_ o8WDKu!juQi endstream endobj 1376 0 obj << /Length 1180 /Filter /FlateDecode >> stream xڝWs8_/vf҄>ܴ pK:~XkwF+7#\EQ~hOO]}*G6`pe"IQDO D} A}ΜË0~?,6,gfZԬW~< \2PIp-0;:mQbvObr><9GSL£Rc#@QA<,aK킽l6g#%}(\֮]\?܍㘆gʪy6"eŻ16V"-Vj9bP0#Ƈo_YpCX+K9ҤD%.4_9+ҊqPݞXhbSHԐ-nuQlyZK߬PMd|K:,s'P:0~YF 蠀W8 ӓ+-11uċu^V8~I XaDoeep4t)%`j4%ӸdϲBRť^ Ϣ6$A2h_HK3q' }w\0mziiX X;_r`BG3|XZO^kMsB/z5\eؚ3z1[G7 5i<O~uWҒE#tf?#T/⛴k<7<7婤| dSa\@,o񎋇t5DiQ-y_*]ů,Km-I -Fkx?2Ϭ phO1 ;[vUݱ-{aG1߳ɰ]p]g@x W3Dh3#F{ n`QX'h&ZݾaKWQ2ck'ey&Eޭlit endstream endobj 1381 0 obj << /Length 1037 /Filter /FlateDecode >> stream xڥWmoF_u@TJ/SIz)Fxq?,ƀ`033d-"@9-D~>iZY[}e8vmꄧ{i1J.(v1?kuAUQ ma8h,>}&(;D9 pŇi$oɛ/ CB0s]PHlҚM"K]^pݰ}ek 6ҁꇗm ueeu_j ^щ~͍ ^A6=lyd)XN$92ϊ-K$Lɿ.y WzYV),"TzGE(~7G]_Rۑ{:%qO<7Y"](.Km4y0鉢Z\X" 1dm dS:- cu#ry}}_Nәyik5>'[6-3΂XAQD@95O.EG|5'\úhƬ.U~! tP50 ~e,[Dy|腚(R|K^չBhz~P6 9 [F#OU_~9գSqK QW9bOXG2105ycw)MþbbQaE1{JRה릭UP˓w1)7E܄NՔ + 4nk.v=4bUb\QLpX-.| endstream endobj 1385 0 obj << /Length 1033 /Filter /FlateDecode >> stream xWo6_u/v7s" +%ijCWM\mI4$Ӗ59Rg;y ~ݤǾC2\Z|+aL\{,p3R_FtC5cDw|Ǚ`,ݏg \ -G@=9_GCGFF᫑lܽɣ>r$2}xr(6lcJ)_1^'^O=~Q* dj$HSҊAĖkGamV` Pu< Pnv=@ĐJH=ݼ %LM m:Ap}&6m/ KvzM٭kB+a 2'UP(*rW{`63Ir ٻ?;=~4t ԑ?K5WbTv;q -xRBiLMV~.( egEFAh̑vs gEMa"'>%!h/}=p8 +^;Ě$odo WY1#7YiTP=\=Ri+O\JKOxuSʗXY*;:9k*֭喅>קmЅV)-揹c"Jy9 i2i =Ș+ѡғd1<]TyM8l!vB=\lE?l7ڭ=Bĩ^ʨ O&l˄*CLv^ mkW}ݱ >DO05;>C>1M{\689bB zM: endstream endobj 1389 0 obj << /Length 1269 /Filter /FlateDecode >> stream xXmo6__!H dWK4> ]!Hm%E/q HJ,Ѯμ>V V~s?S5wXb6MqC[ƨxZ*U+1(M./x!Fx0BhYRf/X AHslewݴaO\oBwpTGca 5X#S`ҤG<],c=GiU(QPăr` L 2T9uKm25V9*o"rA(t l`CԀ PeuiEwqx4 !i| &dBlA' 0)LlZ &798Y;N2e7 Xy^t(45 $쑖S5 o~^ʴ1 @Low(hQ@y-Z[ӌuo?Iy~9`JU4.ˎs< r3rn?ռ׷7:k-9|95i WI} *,jʨKv֏}T5u(^6Ž+%%W)^ Փp$#eF 3#!3Snl' =ymNv_ בu>V  6St:LZ7lb^AgN)(Yzv=0|jlԡLlNkM AF/tb HH ~ hWO"FN*t/g˟.KIg? mW@hU1#Aڅҩ~qt+C^8,ArwwУY<ˌf){$(PV̐&πoyr:{а4a͇5N۰Zdr5 nd6Iߥ=bl(wÉt)6 *>SVv&udSGС!=ϲx㕩bćY Dmeޒ!͡5cOR)N|0GDm Ǧg%a"l. mLp[0[npqb7ôI&zgM0[:֌Kym_5 5\t0W`Ȇu/n /EC㉩;+Ub ]}k endstream endobj 1393 0 obj << /Length 1253 /Filter /FlateDecode >> stream xڽXms8ίp/ : G܄&`Jtz|&/3LჭEjYiWk,$,]4rTdB.;@*6$C5j)K_nК:ڲa6ϣzIô֚^Dz L[O m<6%"2VKL߰bs6k) υt8/IetOlI Lb!4%`oIA>4mӖ5ho$`$c"HKD^&@4Ye0j(`)4*_])D X-ξ<`I/OM4}ufY2^3 loH8m$'o gWAA!7}׹5 ˱N{ UqNhx)ȓ[m|^hʭ$rⷦ[ :_fP]FuPj@Knho= )v͹+Rћ7KҖ5_]yn?h*ڟv?h]STrf?Pa/ endstream endobj 1397 0 obj << /Length 1118 /Filter /FlateDecode >> stream xڥW[oF~WtwKV4&i6qV5cʏ\U>ws.,z~3 C1́k A0t/pd劦pdHGM % =_OIлa! Y[E.W/_@=VVr K:@-{@(*M\ؽϪac7${bFlEt88x ~KзHc:ֈ"RN>yq+0gH92X߿\N&ׁ>^,"TS'W7uN;J7ˋCL$Evב*⢒<>SIY#QAXFK/$|4FiӢ!|Zo4:#-,RNltqA_\nւBlFm~=wxhhGTǫT=Z/cyAVʫE9I8E}z/ٺ@r"k.I#} 9nZ?8!+UɤXtܓ+F"Mi4}9ow% 5=zewl$ܛT/ <> stream xڥW[o6~`9RJ)!M+ Ţ-zߡ(_d+v)~<9"cﷰ0plaxs|6 cu=w |>Y?*Us5H%EެG? zܣ < D;ӬA1Bہ 9̇1E_z{͗ +@Ca4G3PT_fd-p`i S7 𓺐߯.K8tq?J;FeTI.(fk',R.kDn|33i].xy Û/[޴Vs7b۷ ūH_%W1REj5-JoLlkӴ|EO|'B&MM:t`>uFfӶ흍޵ǽ3k6R.4ɟj(x>ֶ#mS[z D&HwB&Nh1e5M޲"^YXf@M#\Sޏv\>4} lM!-;p#?sٕO9o&4%ԲMz*sŷb'nz:鲒 p%pxg j@cE?&XȎGO:;+D) `gģAM*y<"~ܮ}Sk~ 4[ccKf0Ƞ,8*gMSr(##H 0;A/s>eo\[aEer,L!˵:t8t7 endstream endobj 1406 0 obj << /Length 1227 /Filter /FlateDecode >> stream xڭXmo6__fE  dWH4> ]amkEGxQcӒcy@xwãN# }<űQ#xnA}e> _\\^ʔe 56{=KYh.oJ@Y؉BTi-p}KϪôߊ97w^~#(Ju"! };YiyVs%K H w lR:WfʣA$UbdG"P{ n@!@ +#_,qd+>&NUHb  }jtCBp\W`ږ)\b~ -VHc*xȌ8ԘybVSiGLlv5d RZipGUsHBt=<]w'(:;[rR)i]t(ƠΊ=+`D\SS8J0KW 9y4(} ?Ta"OoFףѯc\`bBx|}uoPzUP^Q!x6e)iPIt2W++r<ǐT02}7mhh(CX;d`3>f$9U[:-eT iȄkעsDRF"K3k^u~)D/Ihe F-BBNd&Lv'IZzv`B^aR!]jto8~&8~9"|kL+׿MoO r* _Y01+>OwV'5b'11ɫL~oK''T;ƨBdh UGvCSom}Ʉs֭T5 ^W#K;+k.gZo=nE:OF+>;ktUpci GMVT.N$YYY"f>p8M:XiOZO3h#V1/1yVbF3*֡j3'ޙ֭ u0Qa0s،UnuཌMvl>@0N˘]<ל ,;ŶKBB'8zP:h? }7KYd NdbL?A= endstream endobj 1410 0 obj << /Length 972 /Filter /FlateDecode >> stream xڥWێ6}W0/vZAm 6F}J"Ӯ]M||lɒ֕A499gfHE~3af67ŖǑFL%suLR̃"_eKbGlfD@0& ZkD0\XI%Ȳ]݌XI ^W*CB5 {v]PmA,-k)|ČvJ.`g$l>f`p愽aB Ӗ(|aDWu歨y {L1审Pb!W^\h.fI)md(YY驸0A/ \^?/]KF*XKvfosRXycų+߿yVڲ]wwܭ\ByI>K h?qJwۓ<-^W2$aEs07Q9 cGyߓܠRQ׹C^zۘdD˛7ooo:/a[ۖ3aO3T[#6aG;bThNs82QP;,ӭ8͍ =25wڱMJM{ttnsܹQOFZ[ n%{ZإU>UcjJF8 æAmˁǖÜ)*6P;|o*\̿1n$,Sq!a QSz@siM endstream endobj 1414 0 obj << /Length 914 /Filter /FlateDecode >> stream xX[o0~W vb'J]*ҵ4{@44^YMsAPt\4r#@nY YM@n]],k:Ѹ3 t^"SJo*mLkpk5 ʴ-bΨvuMP'`ñ}rjLa:DI$waMxMs8pmdQT'qLv!HNpEmZv\y)T|v^QW mh$z$XX#N4SսA'LϟgN9XإE=E}6""DM}䰩WC(%)[tL=Ԛf|?7X2phu\rz~Wmp^#(&K 3n$ Fv?,ڽ~C 7 [M7B' ES jAdlv[::H>UoL4Z*)!B ,D0s ,~ʕ`KX:ViOئ,Ci6q: 8-Mp4fka@+pJmO/ oc (>lYFMfk 5 tIպʵL  0p:l2p(h4ÿ So`MS t F4E?rF endstream endobj 1418 0 obj << /Length 436 /Filter /FlateDecode >> stream xڥSn0+E w^ u$H6-e-^HtY@rH摠[ "FSΣ!4QC9,W.ʇ|؎7Yֳռa HMyߌ%WmK"!H u pZqA9~ee iqypҳP] evFFu< `D{X&gY)ōRy# {jOF;dz-vO %w% endstream endobj 1422 0 obj << /Length 218 /Filter /FlateDecode >> stream xڍ=o1 ĹcmHlm!w@ o 0Tz>&=G VY8 %µ|7Qa}ҘE.isD$Vqf,d6z`A_5=|7F˫5쿭$5 TTJs De펢Ti5_}oP endstream endobj 1426 0 obj << /Length 301 /Filter /FlateDecode >> stream xڕMO1sl:E! 9@4?.&#i:}g: vc|gT!Bg> stream xڍ=O0wmsZoU41RIv@b`{X$ܳ.Z  8 > stream xZ[s8~c:Vxnl3{ag2F6Mm [C/-|>|x`dd6*6{c5֢]O@:K?YoDh`@L, ^d;O3qC&fI0۷6 bh.4iPeZX绎F8)~d]%+`MVǮ<< 㡨K&WD5OTHU|r򊟲QX**ԮSI= y&Fp~ +M'Ε{EܰU<+5mgWd6.MbWNe䲵KWX`#=y)e#r47¸eF[[k|-H5mQd~4u%?7j=B5xX =a¥鄫Yٞœyee=b6zu+U|e$(/O&IczgIuzj9`TrDLmAenŤmN&^svmduד+P]p![G;.fvUp*K=DMo#yjXKPەeP{3`SeT. gT)Dww׿X"vEeSi"1w| ԛ@W+kJNސėQU>V`mcVmiZrxp̒N^nR^4Y+u236+0v;)|r^/JYlCD'K9"@8[)[\Eߴ4Ɛx,KFRn2js JD87?o,pve@M: u`F\86]"qErZ|^4R21`bazBmH_?ǹ4H!BCl@ku endstream endobj 1323 0 obj << /Type /ObjStm /N 100 /First 984 /Length 1592 /Filter /FlateDecode >> stream xZMkGcrj$}H"|p%ep}^/ٱ=ը{^WzUTR 19 92W%ȁT}ns )͐Q`X)Zهp$!%naY&60@YT%y 'E +0PT;kji%@bprnI 9W-I YЩb NijP,z"|G*xF9mT lRNj[S ˆymTOMPL}ޖmTŔPJJ@9y^jh@3, DP8GN12v (bmB b* kTD B.hC9XUo;$J(P$1m,.X]Qb~ٴ:9KW"%6෤"a$` Eg"yNZ:!AM)U[Q>dXZ $T%( #>AI0MN8i0#}kJDV!j| a|A1 pB|U#Pu{*hVpgOMxk kÇoW6e K,wDk+< c5~`(׫ݹ|MAIx~.(7BQ@eJ)|v@:AA<ve>vE#('(#(GR y>Rή&(#()4ΏM 61=N2yVg<ͦ&oZSctnx+GF's>;:%i~5EGD-'c(L*{`[>r4!=%#g| :l`(y_:ʠ5K, LǒD䂀->, B$ѝ`0។t'L!bBpſɉS$p|m/Q(Ay{d]&jwߙRr]W'GskU|&m &>-̂|,-Hb[9!)U:U/q. endstream endobj 1529 0 obj << /Length 856 /Filter /FlateDecode >> stream x͘[o0)Rq}wKm-&UUCCo?CBGRB4^:7f[عxDH$s!(Gq0J>(Vl8"|ZpNU~R i||;lHp<>!; \5L時竃10#,Qo>,Fx{0ŨvA d旹FG(_TJb  ,8 K618 Jc!Иz+P!7rrK̗ S KZR(M)pʴ-`Bb¢WJ"(4H2 '"֑G_Q' O2=EHvtU=f^̡&h?+upOVi 9ڀYt`q=6敩];)j ?zWC !L&7e?gsNOL`pGa^bwdBJ v]ZY. w:H1< hF،2/Rss Bs_Xߏ/*nw/qbކ w39{/d^;&gwO7WF Ƥ͢|md4]c4, endstream endobj 1551 0 obj << /Length1 1682 /Length2 1245 /Length3 0 /Length 2273 /Filter /FlateDecode >> stream xڵTy\Wf}ªO!J.nX p(C 031` -VD@*[ *ˡbkAB^7AOf~}, 2 zc> EP`mB'1' /@ȨA0206 Rp,|H@  *C&hXcE I3(FnHprQ'Mpakxl%6{bRO#q`M 1J a@,Y /Z( Q`VKRxKem8@*0̗KٷHtT, m`P"c a^t4')_1⓪qI*/5HP :5)吠!M8c( ٙ?!"'jhc_`pFQ 1:,7*zB੣(ghgC"{bW~MmIi@=͞NmoI燴GIgc4[O 은`e*!dl,MsXpDCR ; 88PGi2A)hSA0^a"֌H4hI-Pb&J>t0#r \ ţJZa⢉U&(#  hXhY^c&BuIb>>"g{=ĄJOd9xdiѭ9q!cH肈>Yȁ $@, y!c%T :8DH o A)@c8ڋz5IhaxA5JO̸f(2 t@Sx!ҍ_d%IydAxhCv.A+OsXf@櫤owJ>Z`K a5Ta>koУ`ݷksf~ \#Wg#Ynyw,gcWksC9Mk=~Ⱦ1|{T tzŶ{l# 4cFU7]t5 ^<j}h?Mu{4ueyaZ ,}1cv'.dϰ Tp4ȴ&}]cUIMޘ1Q֚j Ա;D 4S7 _|tCq Mmi]Y A[f3 0\ѺSgݾ7HpҊa]7ӥ["}yXg#J2N*{ótYOgfw us1ӷosoOLJWC:oL燽 99mnUMO#UŸX@.[*}/'V#'z ㋲/^8mJ~bO!\}:\͒ Qe_73^tEãRn>[7,#W͢Brͫ𥕵uz&򓳋uu'k';ѻRǟ7I'Vsw6KFG쏨qQߵdU^OӯE3N=VW^;l;%Ǧpɮ-6Q8ɹ9 Cj$]]}yIiO8r:DV~mX4!δfFXh}&oNt shk˫\s~z_[GRỤ̄<˻^ܕu=u+URt:Zuc ֪_kp8>daO;K_;_~oTBnukt_dSZbnm$%/߿J*޴hXٽ}| Μ㧥nXk |4}/Hvkӟn(zf%n|qA~z\moʾ{iޕpuոRy&t&8b|ה&T{H?3ػ/>58X;\I{FB<ڌ k7\{]ca:Mn}x&Ã-/Jb endstream endobj 1553 0 obj << /Length1 1685 /Length2 1732 /Length3 0 /Length 2778 /Filter /FlateDecode >> stream xڵT{\Liu&[EϦdM2tbjf&֜f4S3猙3̎"fܒ%XFPElZ63>̜y~u(#BH42s8&ѨLN c2H hd:J 0IQ`O@IV`<VlLiO BԪo?JTa `#zqfRd, `qy`\{ dL|=0=Q,D8hȞ C:ʿ;A5߼b)" R+(|DJ /l10^`PB16 h8# zbHb*(R '?w|y">NYt@ õYA2Y$ѐ\*CsCA>*@ERL( yAbd0 dlo\6jf6@p*C` \0N 4%(7hbђ)QtR*! 4\"XkRSB%1$LcROC5ĝ/фW$='x0ә)8xTRr*n?E}рIeZ= 1sEp*1ٸ܈ 箋mv Y:mD?'>Yuakz|םӹe-ɷE~)z& OhVm&E%|࿵D:E&x8;/L&Ym5iraκ_o=,>b҄~L|ЫμG,ɍ}t6v7S g{iFB G|}NY5#ƴ'`;g[Pr3; 8Eզ7Y@[odsVR<.z.i"wV.Hky(%L؆+^Vu FDN=w]1'ɩd$kQu i}cem⒎W+*RGyYsm{͂TcaQk o'n-n;珣.-h}/ Cy{΅#jms>4xQ'z-6k7lMh+-o?p$hl'؁FZf}뚲R6UaX~ˍSzL z^w]zZj5qcW< ZVRs(|mN+Cܪ;RQs[V}][h#|DŽT\ wraUͧrнBCE][/lJLhFkFن26Di=jOxiӨ}6 /dQg};'&w;tE\ޕl<5ڮh~7ݹU+cےUR>'Al \L;{/ {3j܃*%1@v*ʶjy}ԨyFi޶M ٪|f={C)I*?%yi+&e H_q&uz?3LtС95wҰH -⚾ XuB\btoyrr,&”oNk;| ~ɏ {h(_W݈\@?' endstream endobj 1555 0 obj << /Length1 3113 /Length2 24001 /Length3 0 /Length 25686 /Filter /FlateDecode >> stream x̷ctj ۶m6ضac۶v6F6:n{{s~5+sݼn<*1P΅ԒtJEmL, LLlN@#K{;1# hrv01qS$v@'` y:TF 3H 3R\D<,-\~`fh J+@`dg ag(ػ*{;1 `o;*@REQ]Ia t3:&FNF&.@'g;j@΂DQUKĄ@ :@A $E  mgji]^\MXM[IW77PN_LJP1?Հ\͜m'PY8023:0;38PJfa n)h0.B~5` E jnj83x.EE\XL^hѳ:֯H[83RC` 7(˿k]08@8F3gg:3^BQA^NZT\AU7I{?\\rY9,$5휑 3 @ӯrwu==o_#^[#;Wg4?"i4YKQXL`}@gXTJYbϽPYڙj1ՁQ(-Os`AĂWg _o{3 lN'WߊD̜SKЉ]&Kۙ!1G&]cv65gTw b-jc @߆F64J@S%KLri#3&[ƱEoAgfohM* yE4_j rҴwۉۙ؛Zڙ`d Z+vv73蔘=~ppu5U_JK`8Q0q2% f`XQq@\ ?EqQ7qQ@\ ?EqQ@\ ?Eq@\AFYt&6u?34ӿ 6 VP}"af'/W_NYos?u:i`$ [A] Zc'#6N=*dIe:qA]ljPE f@'K >+Q/?GW{P[c"̠)fPT j;P56FeT n(-lbw@Mw ڀzAY=zn߂Ԕ~%~1B8[5-MA8Yz2\ 9_#ş_""zVn1s3MSt z@E{`R?2( n ,-8ȕ|R`A`K@:e_b]E0fkBԍЮ>XzPr@Y)Lvv1lz[\@}XG+U^Yz.{G]c߃}7lv.#1ݒ,$jXb6ӣ oy-0_}ؚ!7O>򸗻7ʦ˕0l ɓElv7a ˥mSwA8uޢ <v2uthJ"9"1Yε#o]}UzQ/ }y9.e/;10ʜEVBnqu~I)}QӱޜC_%T̢cyXS&snOZ|#a-9C~Z~Zsw>ZTBz.4Iƾ^8IJFL}ʅ̔pJ}JR*BvayCLrD;Tk6ᄒt[-JH j0 ~ ¾I F~]pW= d>ݢڙZAFOH$h.a_Y Yifcl_{O?dC}jj+` @FFZNvc:{0>.OAy֭ ʺwYSNۧ#ݲuywH)v, Ln9}qW0sFq)X$munaBxX]g|_202(uG /.sKf: $i{74nr"X hQ~)\%z G6c€s]{6M~ER Iê#m3Ph%qm|H A&Ԙf[:P<r^1Wk·7Mn0RpWa|)U{wC!70X?~q2.yWsdQ^%]F$K歮Y^=>41TX/cbTI28ޢjL{6́viy'hR~ > r;G1RYsp2 Kw"w1It.&8tX{'׷tS_Ɖ .3&[?@}vdb*꨹Pd4J4=wEq, x(HͦWr뾽*UV({Qj*F{'r%l]L#zrdUABk #T>|KttpZ z,j{"PL6ItXAsm:wv$۝Y.#2׼j2W[%SB{zq(16 9$$6Rv ,~̰4YQw@\|R{>(Z#FX΂l5@9S`){7|#1y;XK!',xyŸ)-bhuohBcSd\kc ]+5xG3Sjuav5,)HS2 t S(Sp)M RGzxZ~QxG70Ē.#l&T"늍IrGR[vύ)M~~]H֜Sݺ9uc+Or Dn_, 䵽%\p%ٺCWh#3} c&ZU^^ A 7U0R&HZV ՘iG!֦ɰ6 Lah񨸋ʔ $n|lL{p&u@ඒ#CH VC=<}C*^lmxJD7Bټ)ͣ??S. uf0~F&"<(V#7{#>Ygгr@鳠60a+yo5WOѫ~V|-1!c%B0|W?A<9CU=s}9=1RVOY]dm8oN;dQ\W^=Sg:@w@U[Jnʝ\Fi5 Y`c\Ra$PkgMR oQXbxK4?݇V+ŀ~Eܙɏ?;g4?7rt;LJ&A¼/Y6ѸTr̰oZLct-YTL|>jWP1~,2,GP𞻜S$+rg5?Gx3ǶNQe5Z,!Hb@,}VOo٩0ˍ"P1 x щ\36͒Xt#;}ւ>c;eV=."z$8/p CкgB|s.wX=|uX}f+z/NKf}z2xJąh6*z{ Kz_G؇V3+»f[."Cdb<Cu4#; Z-rM-ʚg :ַ !,|G$fu,P|͇&U9n- '`|BdRN0$/1}d9l=B2kUY ;-U@\z жYߘ}J`S* 5LT)t懕Bc)(:dFvq%}U'4G(hXY[ma l s9 {ǠCpZ(a].&' /˛/_!d?L-!ZZ-ĔhR^Msik jSwyF쉉ծ:^e%w jVZ]5cNr,ϡc<}4 2/[?4 WvFhh ojR7(mGi,&`N1:Zp89` -@^X^ۢsT 5 d0avY+fSei|s\i;.76A(QT8:#D2H]Jdg0*FiKBq!v< ǜ=zCZrZݍ( ؍&]Ûw88M$/U|L$pkMm#"~!\B_*M`[ꖖdiWdEJBErߥqAB_ gZrwp%kgnUߕ7n+!bo:OB:>a]0'kd-icO'À<e]ĩz,9`N6[-dbRu86ZGw?_s;b}*wΎ񎹐lₕzݍvMPZC@*kI2w\­9\ʗ>0eD``-l5I,+9z#mA}95j~RpTchIiXhnXb`,\:12ǹ=P>Q}uLnup PFyz۳Z93CF0_} 9pbJq/BC3$8&: ŒU]bh!aƅ&uR|.R*Ӷ2 zO&ƲzdH0Vl)V#v\KkcuO2&KErê2z|z0;hm? B`2zMsf"B-kL]- Z<6A?FiL cѦ=u3MtިpWn1zI d֟PlgSQN1}\XRW5M.NV;|&*BܗNBa ~XLmE:K֟Rj RD;p%-Cu{EN2Ɏ}Z2/_=hN|I)t]u{v@!眪A fkOqWr)YFG>FH5dIYb3Wó24g0/HiU6+ 幒&:YWR ek:451'eŠ98IUVU[1(њ'#1ֳe{m})7n,,{hü2wQ#{[Ta8CtȎ&5m\Ҷ2K-8-I@ ι^l [fQ0NO<6Ͻ^< 8ѝhxߙ [ur~:=Mms뀞n8fE'f<"4b q>"iRxVp xn'9=S.(Fzv"Aťל/J욲 R"Y71'[~lrwa12f䎿7ݥI0ӟXU #$UbR9e{#GYxnzw$ >Υo_\KUTOZ (hGpMdpi-=:yCP~ s Ϡv`x/^8p|`2% P l U eDy&ўF"^/Mf;Q]jATumQ4[盀ZIħ+}U,(T4nUb -i6ѱM/У;(pX-3Ir:p|B2(n+7謕ގKV$!6,_qd)eȢ&;0&t>E'4`$j.I`/z]dX2o]> tahMGU@ؿv>ޫRqb3io?08Wޛ@Rtk)ڇuE 'Q!Vt2DR_1ЗÚN8JDvv`~L},ŋa+<-D gBDV' v@37R69|?6KF\G7+֗SkNԚJw[8}^r;Qep{72o7k- |X}_W UX%rg9Ґ̡l"l7MnvU|L(}ߘ,|O'B,@`T;T[AVp+6 O&ԙ7%D BӒЄ|l摷<:HJDeS7@2~3^- (T難x6ޚl|p:xփ ʴȡɃpʮ50ӿrntc.x%dL0wo]Ebƈ愘XL-(;Y9{|YϱqO8ܙ z@o4BݾӈX:w(' k,ʹ 9&=IupɻȨ 0k7 +N|^͆`L.y:D_XY*{.XI0 >*VfTyJ:yqѱ"RUysՒLR{a_f},wdpx0Xb|Ka-S]5ZJ)=[&Wɠ4WA&jZxAtMO was?,[q! wӕjܿNo=jƦL!CP!`^)nbd[]Νb)= Xpkڄ- u P)?GѤ# ljJ<>70D}<@c(~bhw$QCH t?@Sc kS͐+zn u+O|$Eعnu Z캯"Oe#m1 i Ҍ zx]:?+o9^1GXhtX8? vMHJ)\$ =n¯vP Ғa~Fx_I% mq]ɿuxeM]-B2ȃ 23@iWf'`H[yM I #~X3- y2V>lHYv@- Ǔ#PCqR;)>7J!@G4rnH :Wb1EAwO}I>!_;:&L{YMխQ#SxM7}8"H.I3/u/"qip4qhglc2 1ܰB£='AЎ`< J_VK01f Ξ,2ZU΄hw&~'|fG4Y>+ϭ/zr!oBr负5ingPIGk:ӏKH: Iw+ǧF;.b)KaLir 9f|jWƺ:GP>2R#9@-"Gf01Vlq0}t^X5-hj;b@>˝ 0󂱅r)a" ){zRdž+'\_fD4_4#]E:7#hZbϥy+ cTK۟LzإӞ /Rb0R7l9s.DT:vƐ[fS)7 ,.1rH% (jH J \_H}wkPx)?zm3^hqz/ᓖjP`w3D];B֋']fl\ջz\eqU4ekm~pɧxt]2GVitADUk-TgN݇~$)DPHRc)-[JZ|Uŷc/kJ6ib3fS7?l5ʏrO!1|lZif(Jדyg]5:`ktOv$Z&h#WdIS!Iݦ7v7l+S1>o~F#N>Q6R`}.;ʝԀql86mBA]v[ձ>t\h5rH!7ͅwm(*|#3^xgR֦vMJSo`6&E0ax~ Ƹmn# <=9v<(yTFrA mN *JwČswzIӐ摫h`g*QK@␞ َ3QRL&Œd1$^gF=7E$3g0Vc%Nu-ZEG>1T36x;9JTS#ޔqnI#C_wY77X2ΝÏ37+=VwVRP4ֺ3%D"U< `хꍿu4ȔO/YC z}׈i%%#EԊmM@4mU7 )M{հDR@L)xjyu3I<}~2;ӝq*0=A;Ҵsj"kvnb>:h.*b "%ug>EafPQJ( % y@ܪm?J.[ A$am|llJנ]W@&VA7]c,~VH5%ӅXfh=JXGYgA"!I("`,BFnIt[7T#1HVN=}{5/!Os<)mbfΏB/+Kw3@+N`:WnZIOԝ&(ڨ9 vP)Ӣ:/vkZ$}tVBTSMhyLzV ۥ΍` *ZM&JL)8Xx,&ͨp}Uq:r\k7ЭT)J^as%1i?Hϔ p 7"Dx};*n_:V9XO t|iNO-7Կp/D#:oȜCq4m?9zliS3d)W/جGe~4mt.RQDwnиds]<iɀ6и:n%yVio؂{> EhB0^۷m4 j*Iu\ysDYwSkb4sd)feq;Q!%4x@$ ``M;%;[d2]V%MI GIEI^BdقwRC㟬KV"F{K? =r0:D%/yσ63TX6~:!;\]̴Lb7:(SB"ݭ#>Js/r\qۙ:z']?Ӳpix"78ؿ ǷߍZV\a -:QC}IycVA, ĝ')wqyumt} /YM󘢄CQ}s4tԓ2iWa^{h=7֫j;lMaQUj»ȩԄvu堫YHdhn[UAk޽fC7MM5MPw-jbIhy/NթKE7/++>\_qd:7ǬȫL154]dkeD\]HB6G%iim %r֛z Qh3';7v1t9N&ƔYYZ%&a}%=u3;kBJ~|Pz|қA/yLYJ,ȧKYPC$j2)h– aY QviG=X{|JHV^,$M zb_A@iT7gy)&Lwo ixd'% &x47GsTZDW>.hod(Z7>u؀w'FSYNatkui =4ei9Jȶ7c7lIe{~152D_?KBzES1QD^mZzGԊ`¯`&|ؤ㱲`"w:ѝS۝s¿APh[\˳;fԦ֭pӪ_'X]K\K, 5:w?yO; .H>)HbkWĂER&׭G1}W0^0&h'R" Ue=+_hv7Ojr!ե׶,cVwz_L]YC;sxq c/q7 G'hLN*Тx˻\kL&LBƤt"7Y^@W !eq {"7lϏy˶pz5GIL .3]f%Z ܖqG#NQ V#yg(d,B)}u NsiDGk!8^F9h>-rǎ!$Z1Q,v4_j\f`7v3:' (0%{Bd&#xJlQ_v)v#Au}+rdTmL-91c3Ud[Gǫzv) xϦ4IRg6b-Af#V0#0NS>yE;j_$Ƴ-or1w$siBcA, hoBd)6ade4iJiyTCcqTȌmۘ%mӪM<+,ջyi|?1{rlK#塻@z{sM ~"mkOfyΐgc2Bf~oQT$[t xc eA鈍V"&vw%( o:$DJH9%+/K7f$iԥ%܈Nr23QJI xr(] .a'9 .='|KUUivLau>Yf_9^ΑkU+ $,2e?&["kξLy\Oy f-##W',U#5&[ kaAz$I6=`:V7ǔ!9>4618*<9dBݗTab[[b_ߴp}2FX/#G;@N"(~GB6/)Rp>7Yu_녽KɠQ!Y+)[vYVza>“2g*O} KO~Ymq51ϒibA"M@"-~t7^l8#3G i~Y" .t@ sWUsY{k2*GAB=T!3Z+Iq[g=4 +7Ij/Kr9@SyH(wiSb$p#(]ܸ87u!,eoxojM榟qqqr#+xW<}Wa態G@T8+Y;g#\84`~_pQzD7Y_z:X1Ss?d-8;D:y|]PKv %|iMV~ng1Giү ;n }l/ݚt>Я|kA:w(8%8:K.gN[dj!mULf!ԿS`@.iq"-ǡVsV՝ھ8'K6^1NqaGfxA }?rTY$Hs`Yiւr2O76I[I:Hf{ҍΒ=LA)]c˺*XI7AbT֍:0>M)è5t0i=lY/BAe`d2S۾1] j-5b>bh,c@K'dde` K|ސ#u] b֏^v#S,?m ϹE,zy][-{ ~!'ak9@ d&xy~`|2Xp )vtx@lDEurrJLW6E6=ՉG8Ʋ.wQdo7i~U7((:n"'#fAaj) Yp *?ސJt}WhtBte82Vj؟kba >R<~M*k,lJﺁcvS>Dpwz/oy_O?Z狆*QڍA "ׁ"9u {ʲ>+ h8S j. ɠyBM>OiS;q1 f-lIKIEo`$gyMI.*=!Vþ[i˸Ti A%d8q5zuJߌgY6$Bn]e ~Sk0䵿P̤#U"UϻEvg+[k9W DpMǾx( !*@)"%MrYM}Zϋ8OXIcsUzyaTj^i6N[P?5݁z͆tdtuAَ?ҩ|DCoR@[dĹ>g85dtFF $l 2l{郞&xJ`nƯU& u%1} =[.OAoGώ,3= jE KXi}O*p?"it'Q`^s-Q1\- X̴VD *BB.iD.K.t$< E>ZgQ%hϱ%fOW~C,+]20Rsv;_ Z]Fi UGL}j}ܹOz3aWfU {nt(A#iX6j7a+ )RK(V8) ַCu*gLOݭ}DR[0li-ts`3>aeK[prr6 DK F<#l(DDLglra0OÎ,S|3.k%ΥQ:5]kULWcח!&RqVW^I"/̵:#ؼ F}7UOu;"u",,zՖX]R`6-0l5f l.$w}A7w0%gM*z;H6| =֤ Gj9I b51d?|ȿ|  8>ămm1 ]: Zab ИahbJ5;'dP{u"vR!q9bX`Zq2~ZOVk`rTv-q?o M=sAv1Qoۘ(3T |)R`B޳7$^~'פYbyӻ[ZS\~yh~7/jVt5&i@QBQR|*wϹ S}nѧ[#iyi[9HH\-oI ŗ+ÍO+f$J^i_p0LJYBO#sh"ֻ~&Xsl$X.)Y#5#8NZIr} ʛ,=#u*5-qBR֣$'V؋{$+y%f)oyp ϑa;.\t'hOq}^6KBW.e 0]i`Ya' G "C; ٨md\b52O/l! [i) /̪;?9U«ƩD"H 1s5A t>@ ,#DnX6W8aY"Չ`$7"I zGHh4"ŝ&[Jayv?U2ȅ|zn|E ? _Ľ?&- DFNGnL &v+<ǔ,L)v.>LFR* id['_u8?H[(!~9>ˉVLqSzIֿLᐛ]{0?g ٦:SnGݭ: b~g{3GMk~!*Іԕ@&iKhsgaR?ZyuK!Y0ٍ1 W; s׼2nӝWЖQTکsuF 3ߧ'BK$BO38Ikoqhfn'!gDM8<#'O7ğ{cd@(`q3N, C ܁^d.cҰ۔>ɚs ^[Cpˑ#L'DzR H9vC>? 7u[^r9\Zn60gW)Su °;l)OoJ-Y^vZN>srL~.֫gt ºQA^2sj r+Tte|@hG%e" >ڂrȵ ?0+teElɐQ0c _p_ !sb`<ߡqrzӷɿNϯYh{tSnH(};L{A^C"Y:}M8mU56=Ř mYB+4 ͤ˰CbIpmR2BXU1śZ` oQ\hɼ;.UfSE|^l7:?F1iu=N`,jMPš[mVl_,%%.Ss7**kz>^" Az9k8PGmE`uCŁFOV!X9엗8K~O"şKn9KLio2< <Ku4$vq\'*3CѝG,TΎ$QXeZ&$8f2#ﻊ>ڂwUkFsZLFW]5#OmŽ)Shܑ^t.*Qa\єmHqSE`}Z?цCE"Z@=_9*zxۨ؛ ?>}!DpP{`iͶ@Hr}^֥ -RnB֕D7 6/q1}5Dˠ i105Ɗ41wVMΏ߭Ԥ^i(a$/YtU?~GNq߹FDX&.BȠI˄\Ƶgt9<==4=u 0u - T[wcODaܼh&\48^ 1f7 A/lTP6ZzCw7!U 0sws ]`fr>?oM{|JdU:GАC$(h+r~/4IZy(tҠοc}ls=IR0'@R,}gNKfpBtm-ЯKvWڌ"l G QEm ft.P's> stream x͸eTQ6Ҹƥq`! Bpw̜3g~g^Uj"S`p4J;:ؘY@]/,ѕIBlj23r"PQIM GIS0 \YY2@˛`PM5lZӿ UGW0`rҽH8:y|0qtt :uT ojnj :X䙕ʎoB`68Zu!QWRՠch`S{+`nmbjlocۅfG c?`Q# x,v8:ˀ r_;+*9jN [Zh Yc)ilo}2Ta-.``q;߿ٚ8;noI=K6K'ˠO*pu' $sȁ"A-)AETv[+'n,DM QF3:93HZ#G|]_ѯR?B{β5cax.t.Nv@_bL{ŖgLqFaz0w'%L7 ;z=Z`)uϪ *HD9܇CXäf ~$. ^ B9Duu=[Qxo $%XNkWw:N D4¼~Ei9YFޡs>Ηi%u*U%j/kDŽ2F댭R=g }fLXڍkC}nT9JVtnҏmhVϹ׃TPY+eL(f?D<#~ܭgxц+R7 J>` "f՟$!5m}gS(;95>Ipp3I $rNs `| ),AD^2Mxu|:ۂYtJyMW\Fg! ZqJ+?6$a2,Oh5 K2T+>Yv@ysZ&c|Łjl9ɔ$F ba '}/]\#v!\lOxT&fqimXe1fK,.E>1GX-:(m9%0􂺠NZ_ ŷ CC a4Ž4kGm:flcwn?P\m:LIE&sJZ@*5yEēA}YbQJہRA_'GΓ8ɎR.71QD&FXhxw%F,·?! *!*jơ,XU4 O^N~Eb +rb5y4 c,Km =f:4LtTOfS>J}8kG4@O>|`1T<NL37F *DZy_ @_F ң(TӀK yqSɟ-)]I >Db*<8'D1Kqܡb7T!`g(b`%cj$WI嵊D^&(ϩ, А>59km0QWZ :^V;f]O%e;-Rwk´+A$4(%48KBNjjw^ruw%IA_PS; ^kbɺ ?c9A.Cnj̭sIq6azVyv_dfQk )XI P'&$/K}I7*>5GS`PatHh BRG3LhȂU⌴y| hRHF5/ЭdSVo2?:f)-uCY{RJIlDNLc5yx°?/麬L2汮~d2d{FY;T#Q'QqhJh_ IMRN7S.IP̦UѺdRs)ʅb+]Hi ,MY ]Sk|H!|4vmܺLk$kwU-(i@oey}ukYt忥sǹTxkt* us 䋯?(+~_'mѶ(mϏ/ *T˔!A~ #[5!XO~&K=:R{/״%%揂јOƮӐsХޫ/^E[0\]o!z׽,˳t!^`մxb)5EIrW,̅S7Pfܖـ0(s6_dVahVT>иLOb~'NEm>oR-Z!BT:+r w 3ڜ i_JnA˜a><bU8_-C(礘6Rj յx@ ^TX vVi3#WaT XKbQS慓s@6 T=860(iHZ~uuفraw헧%m5QP;-}s:t5k$XSsvzU@aUx RݚeACg N*ĵ Z6J7CvHqlM O{]H?իhˡ]4g,Y舜(=H'K q#΂ħJR V\~B˝EnHReTNe@] 6η0-F0}TI~җ$*- r7-eon}3+3Mh &&^SNP{*1JyiVrplH1Җ:dsh qr PA Pޙ .eZ0hK(>qf`zXࣼk]#· ?r4(.Ps?﹔`  5GVV@),ܚobBEn\tTcr`t>DWp,Kj/1q4O5u 08Wf>ۤ{}0]|{4ҥ]"d?ؒWFaAʮ`DbU){" 1#5B=_.rgDp]ߧoL-Iq"/\<$T4 55|cVu@H֐a^-{1!UOrNJk&xzQ9_%P=}ZXB(֍ h@!.F'?lJ[ی[_9wc4TE|Ƿw C(&h4߾" bLiI8i80EDRüZuFGkPId=} E\/'?icΑz@a+ fy,FfN#>M&HJ[+ )O+hH`\B;Fxߡe &qH(ohDdӼ5\6*1랍>4H́k'ktqn1prjHƬY`z~(5k?@v:4z/q؎W'Fٵ2s$|&r$zߠ*`-RkKھ'3sVؽ ܩBtvJ{mʒBgi^}FsX0]eU.f Txq Tum 6UX5qʢ.ΌoˬŖMyLCsKSxDjox_\>Z;pN,lxSQ{֯7qsP _t/';p%1 )m򸰒MQ(D~:[GD%I/ !G4|ٌpZgl>32"⸔~ Д=z=@pEl=NV9;K1J~/Ψ*+kH%\K?I uT!>lyB1uMiugVJMÀ3I S&X$ڀ߸nck%옛S[iYkDo!M`4a*^m+\ 2B3xnq}o& hTcnGY9 nJ*Tm4D͍/&PHWw$J"/eŊvµOS] $HvwA 붼v^p9Eqy$gڒ"0U>"2S, ˤѿ|xyI:UC̥fqѭ S.7bF|˜`J$e]IV)ڍ7u:cu!lc/φ1XZDiT >΍,!D1$bPHrǾ͖|C9MᡡH6_I YSԻdd>%*櫊Ҫ%QVT2)Jfg [^zRw mˆ &3fY5A,9|;ȇT!#N^q^ͽm*]څm,ٸ9J.ZY}R\(]>'CD| H'?cs*{i(uEEy4h.>5cmO/VOѕ<Y0>t`*kf fT¡{ewAqΊ2xWZ15Va Ow%F`fBF;Z* BK'= 1,qôY$`^5ҎɎD'7KNNL0{?f]>ZkjȘ_| 553mj\ɞ>W)&Np+k*O8•0${_)a"9h: ZԴن͌>nLčSt L8ݧRDT5F=sq>u 0N]vZ2(Emx01lU+[~?Ev |fgyc䐽HNT LrzZ̙g{g M=H?[+9r UUk"E فyVkqJ@eaϻ8լq#+zT]]U;xkz|J+BK01{#$A3Iݡ, "WeϧնLj~8|D O:AKp>˝,#Ŋ>Du!+wy4o&tҜ,AK7y)y~Cg `#1mVp htd(x9𵚯"BAO2P&>p 7gO _Md܇ʋ:pUŷR~T[BSHW^׈~)^?wHmSFmJ6_Lś@j>Y$8. <6Ӌ5t!iT&qT:jqC[eyDw+զzZKދDw{B҈ 5VoV~P3#(v2{\ZgTVgj_,OR)/-^F\5RT.N4؄p^t4'i(ֱæpu[r)Cƴ0IaZ|^g/JlV#(Dq]b(nKL-D;̙Q&# L *\aga4\!!ɌP6F`'2o:;|9ޚcRyjWR S#IO,\$ۻ݇ZD|Y_O*9+OL{\2ȅxUɐ  *LE/0^D c}\DhvmR*{=;.2_pDi*r c$">MiOD)S&֗&KAi\VFr%!Eb/>.x1a8"dG >W8zJdbSq1>ceSf=/Ǟ!!N%EK5qxIX]*/!b {^ u+Y8!P0;py zT@S >GgFw/<خ,t+7Xx8sı@ "M3sN]' *52@CHr5}d `tas]Vsw!Hⓙȹ67< t<޼=ͬJJ?GQE1P}Ldy8~m頷lM&X@9JB *C_֓*wjg _w9= Q uAaLX=>M<t Dgtl-'>GρjӬKd(AW n|- D3m\@wDJ4S\4_vV&Qsfm8Yw.24$i^u4.>)MKch կX>2؜<@й4H9(2]I?mhr7Ӈ)aU~+nk-\.M=chBCѻcB;T1CT6IXxfg['de\i&>&"u&~b}+tr".bs&3O~>Cl>M#N]wb־W#/T̚ w>چIVHJ$#bzCֹ]9&5\h'ݠ>bac8l_һڄW-~O<]Z愖Rp} EќۛUG/$_UvqÇǦ̎}gM7n¿_OnH]p@|5"!BFu8>q4tRiqnANd ֆDF׈$3w wc JI Ru7?5ȟs 0 0?T'q0`ɉBxz2kxwaxq;STw*W•IDC٣^i06$@G(_ $yi:/ -f3${ЈVgj FۚTpf=ЎλOq#"d* sv>3~f'qk{tm\ 7e+װ0ffu#~m3~3vI L{*~'=.#MWLGzTRWXٴ׳#ߨDdaw·_>V0,dt&JZ,_KwýMAzm%}O>4&sZ&:k b)66jɚ|fqWT*r|ž]n(AIRZD#.:{K&YXלq1u>%$.ZM}J+R`twSZsWy#Wi.`L~'Z 79!ikBF<+wW7ulku$VHhyB=+TCTӯHw,; ` 7e-;} 8 \`*51 x3ѐ^q[(ƅJ^igirI(+§Cy?LB* km8t*eH{rRK-ᔤyjKυ$rj_ݡݿ{ߦf )ԱNSDw 6fy:kyPRȞDJ\W *3*aC#"[uQ(Vj<h3eajv#?c{ͥ\D0Gt{m;Ԯ؛~5r") ͶGB*?~Q O[ښ=dLmkrl5Eh}DN,p_Yr<Ɉ :U fl9dDz|XM=]B8ZFdѝ_83ПZ&i/ť<Ŏ齏ETܾt\nn^N'.a?[#zyIV抺ϴX U&88KN~ݯ^txeZu,>Sڛ^4Z]~}yKFpiɭK ,EdW,>oȑ[!Pk_8_d!܏JaYÒ^d!&25_e.yFYʏ,vÕLKUEmn3ؓ3>Lr '8yMP i$|̜?eZy%EQ }FdL@Ox׽ 3F-](I{RMTk4YFCsb}ѶPFm"z պ򸜹* ATs9u0b`KPw~5(_S0 jGV=[c8{,icSqu(.]b44z >2Xt ~8?fX=N: r|ҙ lRH R~'aMZIj`=$pnW$W?/ܲY#{܁5gV0ZC3~DB Qeړ٨bL_^a2-xPM 1o_m}9g)oK;SkHi%{uh&I䟫ŞX^Л՚Vؾ؄f=VN#^sT"{T {loEBQ>8ڒB 3oe :i$=S֜dq$/5NGǾ ~:\[b5̘MxN2&xլ˶@=K+ѰeW 9Hg&< s@ߢ!J1nlQޥn79'(AE8:ŰT^4h%)\ll`?%wv_׭빢QѺJ1`*$WX 2vVcB-`M.=\שB+*X* ,X̍~򰱴v^qۊOm?gNu5h(J\TBp:HNP$x}".;yV4,ͰX&&K(5ףR04CDuϑ:65Gԅ)YUb0.fXbw endstream endobj 1559 0 obj << /Length1 2467 /Length2 19204 /Length3 0 /Length 20580 /Filter /FlateDecode >> stream x̺eP\kڸkР;w  r;{fΞ3gykU}Z]TdLb掦@iG3+?@#82-L\̬TT.@ `YT@`}++"@ts^% D 5 :LM\l"bmic!˟HB=A@W[?Fř&fs<3@L::LV&vGLhiHkdUT5@=02q11]\@s-kд/+fpnNN.JBCSK ))j3d44ʚ`_?A?̭M+IiiJ i'H l.9Z@N,,̖n fGKf';?498.@;_%vs07N/z P6WR@_L%1e9i) M&p3&9&nH3[\aJہIZ;vb.V;ba)s G{{pܮFB =>ga`s7'-kg7?5$Ŀi@te3+?n: l89:,L\@;xF]܀>`nm{JA˺#dp$bsigtef`[ DeGxZh#ji7;;e֠5?!'{nUhj 2GAϚHZP?xy7u?OrJb*J ur0s4v:7 <_\\61z5fGX^?C`Cx,#^߈"?"7b,#߈"7{W+TF`#Ϳ8[Ç8?A|`fX 3 wl7dr7oӿAp.] + Q6?a -9iDL@.֞U_dPM[\Ӈ \ &vp88$tqkM'_@3ąYG3ưR?wT|8:0 x@ BGEY~C@blՖ_j[&~J~(Rb9ZAJet98'3Z[IZ|/S ZyM5aaxΣ  z{1[2uܙpX~e>K OEW U,YN!> x"bh3ex[21z$ EsӴԀ /D ?jOXbY66s eO m@O;'%Nn[><<+ C,̱uT=(YAX|oŧ4 >>4'fğxٞ=ܭ܋tM~*v[ІPZXoBSn, e_L=WG75$ zLN#$a=w9ѿZKF .DѸ-t4UPɔ>W]v1PLC{>'|^PYMsw^A }[%&D_tMMyEbaJ`'>:LdVZXUHk`/q9QK.UP0Ѐg Q.ҭ*YBNk9H sŒS`f9wMr{Ήj%!rzOtuMrjWY1bpP-_6D 0\}} 2^#@s\{n84 j?%^ &nV͞sg>bFb3GOs!J#}xG}1ar{N.= {:KsRk\R&u@7\qxKKH"FX/w1ܜRǓ!ڀ5W2B_,B4Z#X"Cp{)F4HKϼL/Y3ܖ'~TC^ ߐ_o禔AlB"57c}Ê1ԴS='./F+E2*ȺipNNbZȞ=Uր],VK6woQ5WtQߎF)1<X g+6+9m6œ #JҞ f]f49Fԫ>qƴSglYP]5 ˧G3}QWT ~9t`@*YۡB.gD/cb"ד͊ao [?}$zOtKTQ'O Ƶ"g,L9NH<cL)Qs A>U#0H4 lYV_Z[}BCmZI_)e\rs=6weGM0j}-3B[?(F;@Eտ;ʅTO6_/Cq/B(SW^k^7|&|n5;77o MnZ8w}eha f1;h L&^&BmeX`R0P#LԷ,ߙFn6\G#l?v}A'~zc*d^_ʒV Ne2i11D.ċA`I`8|XJgJKӀ (ENUVj_<ԺW̹/x11gX=mL!,x ;QFrI~½bODeڸ+ըvޫs;L>Mcl"}J^8>$&!I|.6ItU#vԹ>xJCC}ӟ!)UrTt Cߓ c]jBG$˪'nUE*|;*E_2T$& ~tt=[89U+Cl-A:vE>)|^ rh0;[5)6slNd哛J2W]~i4,OWTArtDeCO<ݮ HQFy'GXbV5&3w9|I`997]ޏH[cwak>C_ ҭ䉛5gl~! 3˽*@dV">v=D9y(SԘ"O"K1%SiQ˫=Ꭿ}'7/nzmyǏ<7AyH{VW/muH*2́HqpP eؙS=>Zd %5tVUZ`o7?5}\s@,r}i}mY. %d }vnh&0% f_#^>CJm&2oB+QZǷчtڙa];Chw̯><6.r86 __ RNÆy[`tu,b1a@7"aio!%6N5<(R&1/Y٧ɘp^Y1%r R%JY݋[f)~h|jz_R|ʾ"wF6yi9/G vp0W@Wd l]ٚkk,'bx ҩ|& b;l3`iSMQ€Y:v1:2+vi4 rmXa._XzBU+qGMFgGj>"XQ/,iU;O8겁('΀<:o"^pM?8Mzqؿ9t;B? D_{$|D*UZ~{.!R{5^gCTȐ~yد%v4*^exh`Y| Bk O` 섫T6g9XQTO![Rwkk=ő`&:aǝ?b WJ=Qҽ^ktI{eMKFlÉ,=ك#fn")JEM J!m?|yOH)ˑMEPwE҈ l[[Ǹ3Ei)0EE=?ڍY&bZ#xSO*J~xLywyEu]݈>F e1lPeњڻokF+Gx*VQKf3,ʄއ ԶJ=a}҇ڊ~ܝcyR!A٣">RtS*]4!G?wD˵AswO8WC&шdLgk+W <1F~ ޢ(Fǃ,㠦~t{>,NQ?s݇d0}}%ǏJ-wᷦJüKuRR붬0eN;騌Ķܦq%fxjDb꙾ RE:VP .*qrr˩WTo ] -C x}Sh~ȥ Ak<ҟsY+Z @5tـ;Fdwb$&IBA,̘KuQZv{Ĉ(k>>jvsCh#5L˟s hk])Ԓ~ R{Pw@Q4Jw,#CTm9GսDC*XG{?*=yoIOnkZB +:up&:r0kQD Xy/$1$ܦuܗr#j3M%x XPJr2!Lm~y/#tzfQ`€D^F" 0;[#Gpwiٹ0#UiDO3x roOB1-ӫeVGAsn9h< r r\XؖDX2xJ]M?DJ#Ճ^M-r4Ip:ݵ՟Jg=о }tR 3 S(wnbA:Z:uVC -]:s^z(';"^l2]TϦ:{4 )}' Z(t+܍Q|!~wS`S#4(ܸANeW;T%;U*觇hl<&|urXEN'BxՕ qǥɘT?z7"}@ѕ>W– /~9_;U'՟X*5n?1**9C_kd^m/a^@/ny̛] 3:-. %@A#6edq TDnو01vm|TF 8g!,lCJgkwMI9mtd~fzR]xEa$-:%#c}[&9LCu1> OlPsLVִ+Gr(Lv~-[IyTH2+SzYchŇrZjƠQPuMnK eDB'?k2Gf :0tYk,xC^26:Z޶8;~vzNUq4Vvmx}Olmqz'e=ðK3\O~͙Ùdzl,=MY@yO5Gh;بK.GDcc|~N{HǦxSZs[@| 0AMF#n@ɀAx)SR5b9 ՐW]1dUMk)awҩ(q{G_3 G=UngRMk1o}۷R*L 1w Zk՛BBߥ3 |װ_ 2ЎsbGXb՗2޶huۨgf̩#ߓܒ\pc-7.2YCۺl6z-6«\9gk.hBB^XH_Xb&N$5!T}Qk5"p}8^cүW.sV[DrYc:.N?2ˎ{M]HtHhlMN_x]7go Ӓ\.?k|}UWgC`lZt_wBpv$yjX83 bx/.KķqY0*Oϓ|!ps,5)G틈۶Mً_!K\ qU;ܔ$"{+|e`zJsWX-ec .m:q1@=M>\lPhUeMQ'_^Lr敕+ۇO^k"A;6Corק帏oZHTܬ?` ^W>߮^wtY|U SԐADC'Y͍M"(ΟfnqN(#e!*~ɐbzl]\<-,|<3(|ky<lE&ىV!ڷ+ Jʝv‰@]# HްW~7lvOV=]. 놦 ,8}"^NGД40!ǚ8go[!dn\bm iy] K9`!}r<%Ѐ?oRӿ1>:zSlWq24uVvN hCIѺ(8ߥ$N˖2-sOcgq i\hM1z@ [$_ElrmYD} ,!Fw!8W}2U ][ksΑؐzאb {$+mNW=)]q\ت%U]Szz3Q换ٕMtW@[N_ 6- ơ*&+HN<5ay(1e?':5R7?}290bΞe=ͫaC벟p9h"o 0ߍDDl/."))Nq': 1Me(nd/z;ڑBIDٍE.P#I= j`$B A=om }&@'*3cqso_X[fUNϝ)bd;SL 2]ZT\yC(Z*PEIhxI#wM}.dw:~bcE 1i. eJ^͌yd;Œ]BTS7kye]]ű ,}v_[Z C)=.p|>tjBIlnQB*JtКXQL_-%/:l㖓K~Y7yÕ Ƙ iKJFQս}vGK3;AOW{m\ЛqƑ;;$Mcjx?wȩ<뱬T6ح[4wX;n ㄆɞ*𽫶hӁ{v,*qnv쨖;Ԧ.{d:*KʚMȵ؇lVhY|3Lg~%gruԻ՝ݷ[HWt:AAC.[2hnnӤXr}QIuju5-eFɟVok o1=:pM̷W#,909eVeY̱^ь"}>rej2?f7ӽl~-9Wlm|ʇ  O1A*5j\@;=K59EI/?سxqڳ~.*鱴4-+p),Cj?+D7#y^bF0]:uD" G3ڊ8hDR LySFu˽ͅVnݹ";PyCٗg0Lf$S۪o 5T͖rCnVm=b[U"L1_Q=ȅR*LH oA,ﹾ:nU)4Y%[Nz>j,GAB:Rx/Qp|lI$."w9PI7JB[[MJWwD|;R$!( -o=f|@Zm_+ú~#48h@QwhWEFO|mD}?|/NXtf ƦJ4ү,ߋi,^qx&APr5C8KJԋ}QJHG/_4ELv"e杻lw 9%;ɻػgdTvTâY(ȉ๛jĮB.ޕS Fފh H*f,VII-<y({c4V!XX Q-!T~.‰?ڙ;Ї;bBzWn,AcS\" 'Ώ-= ߓr:6MOm#D@ld4c3<╩͞ "~~YAw+fQ?;ZQFIAzn_q_Y.~qs:<IQr Tm/Hcx йKڠZ` ϴw*;r{Cj"P$8~JsDσZء^ E-bǂޞ}}z9[IvR\WذnMGm7D$C+ӵ6iB~*kϭg׾ (P@i5DGm'|VUE7vBE0i1@Y"{}X^K;R^-89mj##+ֈTi=:>xw}5 pP Z9_q-]z#'M‰M&Td^I>iUv[+\y/xyH-1%\>kF9 ,zq%e l`}@1!;\:N{H$ک\^'6$%Oϲko.OjOF['Lhq!Y ƽK i$n1NlOc@|ʩ{*;]\f] w_zlȧI6ZrT \&*e9b:A _%|nްښY+t㸖ɜCΒ,KtLÏ຤hSԌ< }bGx'°@G_K9T`L̗+kJ͛ m1S?itZ6tGޑ.yu3OKLAً_yh.o}>ry>HȇȤ[/3kLxe cZU&?ze;PAHRgG*G2__a{` _u4n̏T K5P$cQ7B 8-ȜJP 얷GGzas/ۀ27K!8ñw0jl狼O4Aծ;yaQ[(9 OC/_E)а瑛e/<7Hy߸_!b0n[朗Xrk{v_ 0p:CY$!lW4%'cK !߽w-W><@?NͰ%z8D(LۮՋy)P{4}?@Ef"|>J 0Ff'- #@49ʊwF,4PaY5ȷ!SgSt<k&kWږBT8G|]ч+BQ@M6֮WDl1\D׾LiH0wyHeפyJhռ\OsdhzNY~=)'A|T1 ~#.!܌=D7 4X;CvEq' 'DXR4Rj ?uz"URdF?EDOv|yEucL{AN-b+"R[;->P]FYYy͖4 +#$Bmqv>FTe+n_HwaVS0 ]/ȸݻ!J,̕8W%ЮaoEe&B?o)P}mACgb{6V,̟JRZDz(+)Kdª)éh0 gU{8UcJI"߂mf!|Qx4(;i-wߵ3AU g@j7C+ggyQnq$3e ?7QE20[=>v2-&AR%TCiL(`͚T(|?=E9@BbNoa׬.E7'NQVN|ގ{d_mYނF|%[6g$\X'hI J L[i'Oj\0G.tIrR`GkS7 tG]vi_\l/եpeM`V#G/ c*M+% oz%!kJ.4Q(bYg5C79 9|LD,^"WĈŵ=܀䆎b S:ɧ>xR1Ԣ) [ B;©_q3wE3RaW*Z@Y?^Y\2كn[Gz{c B^:ְaJkS3Ucx=6tKL9oR#&G*Cþ&ݘՃteA[+ƈxyb}`3-u>^0Mph԰« -<)yG%ECg ͊kh_qbm7Niy_^IJ{,Z6e8Ay5U}iC_Ƨ¹g/=\vm/<"<*ӕ`&^óu,7,+{5XJެA4 ; "B 3\i6%SM_f=eg1h̞4/t`J|0`a.q3Y_jo%z]:yDP~&caô3nWy9@UXO&*oGWwl_Y@.;-`f2 \(m^NZ}.qi_ܒu8FV@[3ړ5#+zZKlmyi li_PWhz 4E KM Z[x I䢺^ҏH%U aV^Q@"S5d_%B+$1*7~*+Y#o\\MN#^^mʮVu-7/cI.D9f%$B(2+ >!GdfލFۍU0dk?ȪAQ:tT${dh<5nfDj :rD);\E޽T1!Ys֌$}| mX,z}G2xL;qeآTHDV)e5ntn/\u,#U`Rs.ϱ+(>}c+˵.]ut0㑾 Z>4GV%?)rG{veh3tT# C;fJ3\O}z\XJ<헼[`yrjpH,SY5-KCˑھϴm탑E#J@g9cc2#L򈖰d~WN4bZHRMC*t , $۔/-GsTqPd . z:=oN覼r4\RnH23g'VC !OlRUoy7%lzzZIW^]@sKS2G,Cc[K@ͦLve)XqycqN8wck-QgפO 0?[=l96iPܠex=tmq ag{pi)^+Y m C90BA_<>ZjiE|ZnDHiQ\*7͆NTn팒jTU6QV::рA$Kz &%~h\vg(UN'tG㟤cUd&oPE9 iXƼQ>8tɘ }/YhP!/Bb/cjmR85Ң+cj+#[= fGM"1^xj:]Lypԧ6U)TO~` -7jѾ9t1K$W`wUK\R)`6w=lk J|%PO 5kp1Vĭ%6JZZgC{ͨ,Xy۫f&FLմdkGO C'k }Ĥn2Ez?rÝ'M ]4XF[Ek5S6WX4~%i֟eeN,za#%6qb75}z bL%)EނoxZ% -g |l^P )( ?%]TvJ0rlO D[^fnAL<BPMb@GXSz5(_?$l>~恑8GTo/͏}̜bڟ endstream endobj 1561 0 obj << /Length1 3131 /Length2 29547 /Length3 0 /Length 31269 /Filter /FlateDecode >> stream x̹uT.LwwhZtwt7Hw7t H Ҡt4n15}9*2uFQsGS#+ @#]lL,,TT.@7kG 7  lrv"P@` Phx;Y&+H tvҁ\]-~b` :@/7+(믠bL93[GOW[k9@I Zh@+; ji5eMu:&5D 02q11s<<&-cа>&VLݝ]4@BTICbHkk04@ߚ_E~:[rWx"V(J5 Z+77'>ffOOO&KwW7&GK&';_ 4A]l.@;;";@&V**JIk0L75IQ EU~2 Ε =h⠊@I4_S -@U2+/&RJ J꒿tt㿵w ** {k֙8s3qsw>@s*q9x85h]\NہDZ;ï `}X@X\T+¯Mś}[GOv0e;PVA"?2Kte3+_)_b_bP7}&v@k hE]܁+!r̭@DA]Tgi?@2sG;oZ 0+9VT-ng @?-MO0"O_XJY{U̬?rY7!uf[c: ?uE7u8QAAU1YFNSKW,oCI3GskKІsL\\LX@ es0398\Nnk=8̢D . `x̒B,f?,̲(ʠ P>!P>?OS@ bkA:(!^4@fB h+%k! 6?@ @[ ZW?nb/ ;7hv&Eudja'/W_.XI [6SVNV@,@2 _[ۿ Hq:( 1dI :`A}rS FNԠ`N&O9@:]+/ hgwGg?䬠EVP5dj(SW7ONq7+_str69"7 (_O W?)M?WXmmhehbz䠯Py-&`d`V"# 2%uus%m/ϛW!rswAPi V!'rTnwuok8_n2I&ey]=ds!.Z㲔/_ͪQǕ=ҋt_`&OoUE.hJi $먝]Q){oF;C|`C,uVn.DLČ"[E"r!~cJ~.PD]zwM{gi]WcOlN@zc#Si4Z!̓j4;[ȩEQ,G0Ƕdzix`sC!{sC$ ͘tanOdN!EkMg ,C׵6R{OXخ!U#rFD޺pՄg7ĤM.M*}W3Tp0̮i.՜ S6"yZrun/]6%Nyu*V`+JU;}t$AAv)ZTMzR] }vxkvS@ "1*ԤKI[5P-&/(5][GJHzr#AQlagڬ·zyY̪T*MrTU\+=B:BQfoFvN[(3ƸW)n#p1M5Ir[fH`'Q+(nlXdĩ1C?@]guw?IDO'}iΏ/BgY=Ncx{òXA!zDdb͋w<#s+j1@iߘ +Al씣MA͎6 5-wKBNH>ႊ5xV>dh)AH!< ͅ'.a k3zMESÈ :\9rYGJ^2My "1nDvvza"zPxm%–Y`t5>$L@0ZLɟ> PU֝`־N +k:irv DŽƖlԖ$ٵ~A۰4VOzE䔐J6a}K3)DwD:CkNs=QmG앹Ql9?78ͳQֶ` =X >j 0S8 DeqFۇHjvk8h~4S!ċ{ &@Cy}8KavG ծ2˾lk:uGڀ:mDi緣;x _$C@q2ݜWO+D4J;>:zaQv7~]H9%[OJ0nV^O)[QM鴘*bQntq8pر_'*}TR;qFR  4C~ur{t7u{ L]J49nne{(+I#H !s1U6^p2't怅f;9!BVLD #cITIϕT6+eE{KYZ_`1İjNV|N.9Pߨh`ki|ƻEo?G)aѭ;fq4=fUsҫr8/Mw&9U[``|cǮӧ+\nk?m16qV7 ֩h+٦woQ;8q>Ex/š'7 '{/,hvD]'!i}@F:0P UvS9&#?FU;^M=~9/ sn%'Ae|`2u;?.4xG`|S.ˬ,.m o277e"XR9oh5c7v5ׄzvϠWItXQK?JdD~H8gK#~)uh(CJ-,GnߡA`tPrY6wQeseCߺ,*qӊu4J$!&^C՟O?\) زnEs*C2CŎ%1Z Hbϑ !3{/)eߊJ@hM\zwe Fo`TiO`j|LTt1b{jꇐ*ԊdH>?Y^@"f HS=Jn6 ^ġ!ex pJM o׶WNO;¨Ĕe66_)ce}*aCL;P{S6}RhFEt-2yʔ0k=m,hOFe^ٴP##KHAxvݗi;601vcFj_RzએSAk$&CeK >NDF5xBoK{nzg֖̉Ko3-*n5xK%a{L@ɚ i O7q[#|.4I%j:<`S3o{f_Ek!*Y2=HsNj5 Q ɖ CACYK ISh벉95`Lpzezrs H#X.DԴp(04>HeyR^vÚPY*S=I0+eԽ#xOk:@bf:ǡX#Zh6oV{^f3|\ AŹ *cZ P3ILxX8"tjzO~m@|6⃯5Vw|׾E6 o<wΈwdM>څ9 02`zo'Ȭ`1<%7QB_Pݲ C<հ'33ՂތƦ)eV‹Gs&*ِf>YcTڗ~ Qrt-ڋs뙺{ 0ݺѦ;@F%Zhw ~*̂iA+a cQ13 u`ixl׌♔У"$&Pl6#&])C w~ԫt*]B-/[&ƲߓP#7V2`'.?\L"3pAѻB7ޱU*:ƨez/8#',Ϳ.|ϛΎ]6/$>*bAuU=Prޥ1- !: )_P%zإ Hf"vFУk3ƅٻ$N{0ҧEW X`-PtevXuCtS=X%C[gByvE5S@}Jϧ&mJT^Cm?5=t`w}rz75jƿ0BR G1ݼ}J-3%~Ee\7 bU1Sh !';ΟY<!l>:1zD)~|zeuǥ@zD'hMrUtMXƍ7F*-) wq.-~9o䞻b+NOOyUzV>X!^e6(lI3H}65zkfRڻ7.5)tC%p0j1 l=brF>z6_OQ{mT| ϑa)=K7}߀jYZĥ`bݣTЀ; dR[.k)߸Ϲ֡@_f!db oԻFb>)A9 5*Y3Է߂G+DD`I,HK9uзk, b8Ypin+{eql=uF)v${!aOh8㔈nTg\RxP )prq!XDk="qaNɘ#6hcsOM sK :z"7ec|+*T&n&uN;DX}m EݢJ=c6z5ܰ|*hd uYC:򊤪 mG{v6 rZ~sh0㭙v`,Ǜ.go{l} #D_y9*NNtPYqi~eQ66.zeo!ld>dk%d\K הw:2" ýw6I֨1ݰ\]s塱R84Nr⡊aLD]%\]<a+߭+ctШ9lr-i ŮDs2Bs.Y*V;a,t MGs3д5>Ub D"RIa얋sz*s 4zy כ> ᡭt9)W,wBC_zIPc8qUS&('׽*/򫲳h5͑$2֖x%2~Kj Wey\5A O]xsTV+BCᜁHˤd!\LNdVoA:U ʩ{|8y;$$khSJ^L _jB]- (t,閛eQfݠKV0pk0@Y*$?Eb1m'VT[Ny8VMw!*7?M̆tYf3'>͗ μhvl>:~t vA6,di)JҷCE0hSq&dT=ٻ>gl-L`!P>'QVf{RU6)3_nKxtvE ,aY!kMXp))zu5(?Dk#]~u,M:z,SRt%/l&;]!P#SJUIWc\h-Al`,%nӻxVb}"UC(D.mO=R페jͽ6]p+V#7&2Spsy ~CZ$6tMה:傂B`=+-30S$*"QC./6Ht|UuBYâD1p<ځ.=q@o.|ڴ4񣔗 _*?|F߁YWڻ=Y2&:gd!W/=\;)7mu2FdoJl~㮙˜9h.ԡo σu({r9-?&|S5St~pf̻}0N&$V:uu9ԄVtZ HO'1xx^&;ס.>Yq+&gnQ"Nx~t}No['iŘp #ַqձ=>w9D|r~nh0/Ք/ Ѡt1.xp(G0YN^$v(I+vܗ3eKm s?6vs |Vzs/|J' Gč=HQю|n3] F[{9>x:[MR>Yuz'~ljjR=DlbR8 GҖ4.|KYcDu3XO : b Wyu|\0826QD֨pq,ix+[J^8*"vN[Ә[UkGxW!W:W`zxgpjy5OnD>_O~}X*+8CF{l\gV+ȗ! |!Ma+]DRڸ?VִpWe]2h{7f} Һ`bs1Y/N{Dyc8P6Ly]C]& ' @kT+Dka$%݌"˕8JOu O] {״$;1biZǺd3 LuS=KcP+3XW!g%bgt)1bBhN,#s܁K4%cuR&뾅;W+9L'E %Q9bnum\M¤Ř"ϙΡ6&i %R(h ƛ=͇CDD[xY c~Rkn{"uKk<`ƪ sԒ6 bIV 9RErB rYYCm+Fd?,gVfWn&>&E(|Sut2ZRd?:l,|QB~6g |2I[h֍ubrJ5R a*Wħ]'6tLSLM,+y/DCusV*϶8x79V; G>4@CA{/A"ƺ`_-!jv(7+(xQߋnì'>#~?1 >;tΉy>8yTGZ/ryG6%FX|1A>)6O.+3 \$ M65tք~𠯭oK3HLj]`' 3w]~B DJXxW2y8T,0S_{ȉ0϶~AxK]-nş6)gJm7QJ*ŁђBP;#Il2{Y~:X X+հ}bQOĨ|hJ卪$ c^ۯGBدhw|U)F$gl5[sRslh@̿ *\sX&6-w" +Ccs6gD󋶔|rz =JB9󗄓MZ Ws*<+h'}"iL:,͖88ؤugmf/4`tke[VV #`qZ=A'j7[Ƿ)Ӈs?JW,[((P:n ä(8}F /'9iD+aKX{@K,OV۹eZn;g=aɲXv ;Wګ-F6A,햸Ò~#YGqkIW/ľpDz<6P`u½zG |"{S`ύa) ^ܝSI%kH_P3 WG'aMӀ%ͨQ_yn_:B:'eF ix27 3{+z;4uo-VB<9.yKi eo`%dY&tJ~I D<ĐCc[!t% K#zb|,y0Ujzgκk%:3% `O]*oT-_SXDcźvzjR~:)|܎yGv>7gU 4H"+AW#!=.9z#Mv@omj"cjiO"]x00~Fzf^|s9a/E8ʎȐ6 ʦmڕE`N3G">u >S1Yl>1H/ܥ}\G?2o9,ENv4Hװ'ZErZ؟瞲ƅgUks7.nL*o]N (t߰NAe>[[@y(Ɏ;=6`-6p |o%W"Q5T~ u-2z5c@!GAou_uDFշf!Yv|0Th-9cFt"f"U!˭u$.w%dKC72ٟ>; /RŅx^]@ؠΞ…RȨ D|AeQK)20q5v0TbE_ CãP˂b[䉁ӱL_aHg'o?143NuDZur}|+@r8 m;BAd_}D r8rZ1ՈZxY)lgYuvSKk4p@;%s`~>!q|+Y砍S!!nBy !?@dK<(Oc7&{NN< 'oO+|$&+3bdmpEڰKcox}_dEXs[G,5UDG1FiגUW4[ >G*\u:{ĕBk5a3g3J|`LS^xYE갟Q["U\S/Z0 >Om7r8݄GH=!YGuD$3jU_K&Bɨ_4¸~ B x j tՎu|Ń$wa6/y9{nP1h0t9ՋH`Pmqj1J n'"%"h$V, 0<Ǐ# O="m=gs'"uIԢEx}4|ǵ/wj;kcD,k4Y:Tbf‘F2;Hb-@!ڬţlxT)MMWJ+^w=+:>3'$f\e6f}not4F /%ƃ$8%7 ]Z!0MC XQACdw)kə&'}hkO΋Q mUtVFdșXJgd u; UVNuK\_#^0´F49Ml܏:wK'07t<%7ڣCD{g'/u0M9z78's H%#:%z̥uJRE<(I%邕گਥcts ,!蹑\8$.`1VA1ZVSp3j_He8g(xu˕HN֓Fph~{,?&ee06 2ĒTH|z،™y a=|J;l:E=YBhrTھ)ʅ;668Ku؋ LXQW߳"b%vŻg x n:/[Ͻ)1c\\|%I6L0WauN ifzde–LD#Rcȕa џYv`(QלL3':{h:>"yUjjw-tЩaL/)%f:nc3deh_h/ !Hd}̰csy,PPKkv nTW6@)8'n kA Q:'KH+vPH~uxZy3Yt.Qs9h̖4KDI\N O醒3rwyNH}R+_~('H{ݰʼvZYK; EDd?W=ujG\U-Dq*e[YKi3& 2QZƕ2]7o1jo]bĊ= 8]UkCw@)ǧ2 u6~nwN2ʧb6)uWT%ЄF韼u ϾUv[ aG[{-eimplţ U[2Ͷh[}%׃.fLwxH[8L힜*t͡$~vDHT'LX*4/>+ItAE#KThT%]N*ዻ 7[)\ʎ4gJE\%#i1>mZ>Y_k܈Ku^̕56k~O0<}tj9S`NxLŎs ;3Gx 3ABv>S*I-|)E @χ7ӹւTh׮N 'a\R҂ߒ\$T3k5?Q ~#}`q8䫤'W[sHY-yeg}-hJW?I)Tehc(4a?$ש9!GkG=Pm+Ѳc Eٚ%w/r7XeaxԜBXJ\ ̯ߕw wخC9)%M~ ͮ|`H ~vX2ɮXnAMӊHߔI~^ g,~ z}XLd{V.J9ayA(_OwAљB$.~BD*7t*2qk"g}GBzL"Z*ZJW;N@uE#hUU;$%?x ڀLJcC\RG#ԶypTwT&7.Qmb߹y\ ky£X[PEy?-q8%jE秉~trV{v>x̡EѱW0i|(y_}tqb2Kxӽh wdz 8ݧSyM]i<"BћT.ZܹVEI1-UЈގ01ۈ뒻~#מ$iK2+-<.rv ?ǪzQPj~fuz|M#]LQR?6>Kw̺;>o3:uMmg3J̭|@+g h,.Vᨳ,47qL,eZ]^6}[Jຫ`=-+x 7h%POϚb$Q܌ъVRi z ɨuSl\BbMv`l?1efr?ҼQ}AU~&&;gTǪ z 3!c|kS`20zyDH| r ⢈똃&ڛfѝip~Vʣ*YCe f>|t';xZK@bo/Աhj'Qo X.W[bXx@H*YCz8MkgqRx8cN'ܲ6(|~g\ Yti K#jz}\rS 3YH3$f~ӾA QiF!TQUfqs{|YL:Pk'9nxӿ?wCWEF©obԴB>L N U}4vQAz`IhI;섩ljb+ UV]i7n)5Ѐ~~lIl)I0Ҁʀ=wL^hd&D͝_Q'ٟ.lAb.:G;\m$QhXd)IaF6H y XDDq5s#_7* f+RVۈ BԻifUPy]*:Di{S fK+X~6c}/_ A蚆,C80}Il53*" '[}?>Z.{k #MW?W-惿,초W8|T e8BUW`~_Bh`JIꬷ/?&n{6${;pui;,DொKaz 4{`6m09 =s0ST0kHDlwؗD%禳vMeNbdm9Ux V zdk˯[w_64gfyX`6E,]I0nir/ Ju͏A୒#acC1XwlVv4JU - 뽦wdłRR-#JJ^ن XAʧB.wRdxӻc9E$ts*W!{ KJE9RZPsö5:N?oQdfj[u.'$xKb[bzHF`-JâLTmo.җ=:5il%z6*XIBcaIN\)heS.ȷA~DEfcqlm߉͗F(M[7͜9ƞ,qS;L˂? G46z9N%U4x ~'pSgQ~'Eۋ+y FY:oJGӆ$&53ȇ:щIX.27BВe4 ![41.3հ,~RZ ȸ9A8X%5hͼˌFcPM#;qxIBW2LRJ ܡ|QAXrүko&J$ 7] E66JXڭ{ 1 6:R р,s&u_#gJhKSE5 ѩpщ6-E]"M%)ɹϢrxû[BlJ'z-c>R*NE;z4G.dRz ԥO=3 dX0хţAzK|8>&){JX/=sM I * T|ܓѠk"PWz7ei"F aq],OZCgȫ ˻)QC3Pe$T%̶K$Ӊ{lr5{OTs\Rjp_'BޓIi ""e[h0N3~ B0d!4F KCnH[1RYRv5vZy"qBqRxWrO?ίUݘ,V{SA<]G!/~Ce"S>="a?s)i^RPMks>ᘁC%Z~6 ?|P,#))澭>U\duݑjNf2u⪸Nu ̤U&,:2eXv6Ro4\%C>SLbӘk/KD3/h]BjC)& sGlsIOI>ʢofvhgQ/݅Qaɗԧxg*-5ܖ-HLNr-0L-sOTpJ~m,hcK 8ΕLJVqMژ!nWָP1N^;/9aH;7Lߕt?Ao,VS#XՖ1wҹ tVZqT=Xy'@5" &M{TsJ8nUXoΉ_{z!pf;PiO>'3d`j^>,ܿPP[X Oof#:s f7xߩrƷ]B:AL^]/u%TfjAO .ȼP?MR712>?ֱ [,oԡf `BM:(/S%4d{},$fEav3ҍ>Ͼ@ / ٰc|aiXŠR SiY=@% G+ʼnfZGp,i18p+h J/;T`pq򕫡K[=c5xk'ՌX*2๚au 33TCQ0)l4@핆  S[a.0_u)*i&[xh0lR1s8 XBn}aeh{͵ׅpتen\NVu"(ʝ꒮1=d>2Zm7=QBNDČY'H| z;&9ZO98`]]fřQڎs3︒cͯ,@ d_vFut̤YunkoE,ɂ 2, UV%tDk+Y>v:U+EIWx>=TK><):yd\>5-w1[1,7Nr6Xe&0!X=!=H(΋(bY ITO- ,09s3NXp" HEzeV3@NR%?m M|_ApJf U]ȼ[sJק."&ĵlZN@Rz 'ٶ3\Sj&ƥ`s,nJ:x|K@ &(;Ry(dʬ`73*_TDաױ/QZObE0,~#j/Zo=]\d"n+KU% wx@_:}2E+XRhGjfٳ|"5Ξ尢j9EǥN(n3j5`ÌdvM_ 9%RA laȡJb1Z/n C^LN6nWkn3xaf@cZѴo?~NZotB.-Ÿ ۑ5HOf - _1O8$ئJm\Z&= X7WHȑ? |g C͞zw{(O;aF1uQpkրo_s@&-;Yd*.{BJq&>8nB c)G9*;*RЭ=K $@ .ʦzinXbxHv@&lqG2zp9?9o 2ǃ)"T$q߹OeJ:i<-W}v@Fp*h-V4=_OEU_iޚkե—Bb0]zC<Ǣc9ﲙ Pys`&yXow^-FP[=O@˶]?NO=t Bܾ"w%̵YG\Ox;XL^Qkf|[*i7Ji$>AVx&|s$Y3ĴB*KYgWUJPrZWP:Wzŷ@$G <5ژܲw,,qѢ}pW9wR8/T㸂Nipo!Kp&i´Qp1i=֌\$bjƊVV;Tz Q$j<-8CRBnX?"a'[)7MBԞaP{|^6p8:c^̳!۱)AZ7Pkv>T۹я*Y5<;^fIߥ܆4A$kC4PjmWCX]_6MXzu6_&XD@$KCd[@qEq6u?EQ1&I6'x,$wjFSfμ}lƀx5G^#7AB.?/ugn9kEid돓ˉ 6eZ,oo+:ѷͱ[.PXC_gӠ8vb.WhFجWvA'M)LMGɑ16i_R+:4&(bcXM~vR3n-ҬV{Uy[{O'2m*EQW$xS TY3g)&)7~mUwאj|Y}$7WSlS\:?ٮsVm AmmwАq5c X肓.)%z2uyɖ)L\D(S:c4I{1If%lULc">v wRWTc;E']a%3K ]j|9&n<؏ Ml]S7|޼kvr@҅.:xO%e!}{5ǑSbu/hpfi>e!SK՘M]yNآѐSfiquޠV҅'k|^o66'%&A[Xiŕ}9N)91:i}U%[(W;@bO2#- E{|ًUΤ]),)2lvn/p4 eXWl^C_A2]l/|{PƵd ̲{]!Yk֩ 37wo7bW)wRGeȱmiRMu'Tx Ƌ:e @RCBbћ l<`OO`] ͨi 4U@A2mTʻ 7{$crpWgMKL ݭcЪQ{9U-SO4)nw\PT)Y˰OydE5GbT܎ѝp+6eӚsrrTe/Fqe3eQJO+gfq02O5i#B:ŔP%3Cx\'w#aF[ Ftk._TQ9ͮAxP/»`pTQžspL+4nejerDR @ن 75:uC.7xBL{ogL X<  ٛUuPD*V+ܿJWtKbqH](b;iO:2\>YcÐiq㳍obgUᠹ) +v"سB8W==cfgg+S&ilH]<BؼWBH1WA!nLjxjyzc9ttDZ{=& 8SOr\h|'fx[i9"2% +JcT@|.E0IU&"q+-G#w.\Ԗ2-9s?&p'3NmK~ɵ$m V[ozQ~L\Eڳ^ 9_f4I .C[ 4g8̉Z"F]9rD:p:oyn_xJ7NϹp nÆm)*gq6LDjwZ|z-rφLG'Q[}q?ln9lG;q??dǤQ̬ ^.A{}VL ?fyAMJBEemQCQ!fXb ӁgpqjԂ 9mrMVmVAp7 {n\(;|{;Є0v;`RgXʀ7OR{7nӷFgmڞ,KBؠmpۓo[hh&t>K5\՘p/~k{rSn0u)>krܢ2c?n(A%04(d>qQL 5t8-2VK_?-l>bj6d&+ P '\t4>YYDFWὩw>DDpk=}3+b.yR_%! x<&G+bʫ.ؑo>$] 4ݮ{8IZk2g5" (zJЮcpc B[@R A?&e3Tlp*1}bbUK3ɬzKHJU0R#dB:(ȋ?x%90NqI<*}}FrՃ|& *|nucJℾ[G:&,]OPZGo刼fk=gƗW]kK#7tU_JEq],pFj¨ ]ZTOhzLEMn}nJH7[|JȜy/ڿ^p,01PR4,y|RXb7F-)8ƘݷՉC2 oϷ8$a{ M ^)VKJw6'%SD-\>޷eۇiLvb"YIn7q v.465'BLj0$i+ݢ:~ I:$2 ?]Ʃ.H=yz*YpSDXrZ]77_KHi])2g۠O;E5JOr-W\_7ʝ>XrǶpDmMX,Ify7J\=ow2No X٠GmOS*epke%v Ẇgc< fባ 8PG,S"Ԛ^Hm9e $1.ݩU==YrI]_(?KM& 6-ͦa 0!^ִ/^ \WY>!,%̢&_"HN22ސow!F_?%D ˘J Ȋ98}ߦ*qȏC|]=Y:xƉ$5Ay De1]5kŘM~rp}w]n0HS, 7:Kˤ!g2=cgej_vB|yJ]BTZu S:OE#ByϚP:Iw{ i@Ho!k!58Y-wع1X'Y,p![:?" s^YYqڮiybO#<~w2{oHdmRi fvkQ{&#}%ޟc.& y̛&)fW}_%FH/M."wZ2 W`:' ptIP@ 9iֹ¦U0don 8•$R+LB@I +=,H1Vce9Vے2 xK5k/+I1*{Mp  /[D;̒ZR9P.f}IĚCF@8l1ԕFy޲a* y.Djh:R貴ض}M>c X'dq}an > K9mcX0!*Q)??"PAXҤĎ ݸ$e3ӶGɚyg#-b!5b-vaڗ j?% 2HlTxQygc:BیLj/|ڟG|_*p7No(rwU znr]#Dy=N%U4Y~B,\ռGi<')A ٮǗ;05Ɔuqx@{jhw ?4tt@P[/ */ZL{fGv k<=fJi!= 9gJ~_NT&)JS HOp$1:jZÌޅ@L_;`*fZHk(۔whMI=2ƲFuQW, 7/r6EJ/r_CƿL[8@ zk VcbL vx|WdjN~2ƾoApfO.%<ݢBfAq MQCamͧ 1~jU?PjsZϚm{_d},b * Ńތhsl=MќUZΨ.u1t#ggs q]\3p?XEc8wCOU44b &ӹ|xpƫfC_}++ϒp( T[WAGdih+r\s~YΗ ',>=^{zۆ<$OvN+ Aͤs "@ !4WJ{@&-$2 !M;z,Ar$yゑ̰Z 4I{AJ9F&6{ /s5/u蕶ݎ ic}5kv-dSyIbaҜPZVLCQ.&I;#k/C|)EF؊][j1iIvTB &oܲ[^1kgAgRSZװzqzF FP?=<[GOStRwV\+!# "3 d032qpF?(zܿÌd415ģ }aKiD Yc/fI'1޽ݓrcí~ jM| @z F endstream endobj 1563 0 obj << /Length1 3693 /Length2 34231 /Length3 0 /Length 36129 /Filter /FlateDecode >> stream x̹eTZ6twnScͦCC.N.Is{~~آ6yc2q[l+" +8FF@cs+l]7I'g#:&!v9Y=O ?hss vmdmtAs: | ؙ昙{^;3.-6H(V07~fr#bf9#O ~'-ahұq={/ ?څG36d?: `#dL`n9A.(m8=`ՎF? 6{ul^OO&;?Q* [K1?U a_I(jh ۺzг`z> 8F=fmx,냊K^Rp3^Ho%^'mI̷N)xGmڔP>XIp[ΛELh8KA/]nާfO:+Wm296/q2~D]Ҽ4s<Ax 5VtA›) ai]?qKNٽ%KZn&(kƲGfT)%9{5{yyࣝ`KDZJm >f{.ϲ%CN.z;3ա[Lʱ,/騒4 \Fp[a": 7ΤS[K)o\ILcoo4.d.&P/1bwG ?昱χ$ձI8IO)|[FWxD0C[j1$VV]qnwKe  =O \Qht]~_bY9]'>oHdLUā)oFVY(X\WWM; 3RZ QVvy?nԙY\y 3VĸJ5憰m;TIB$E˴:n5,S@ߗ).XtO8'c&1FNRA]N P3T֮nlk+ÍלnuaAvhpJ%bV-vCժ{ylW B3cDfϾkȢ|;۳k2~۳;WFdR-F[%/| b?dSc1 &j ;J~ct[ 5|ZKOrtP-uU(mam-1_V 2bu7Lٹm)%C&tM2Vlm(XMwWl 1;2GC;'97%\V5b_#Ŗ;d`ˎ 3%:70{]7Y/'(HSدrG0moNO,nNY^n,x:o뢪T kG*+7vKVC׆RoޝQ">m{-GhglbWM&R2!rȇy:9Ll>7ۨ:}/yS_%&&!bZt+FCK$#hk;O+:Ov_íXDe)nmzS|z&7PՍ|+pX>-Ǭ(JKz`(QݢH:RIFF~B0o+.쁉'v<HZ,Jӧ$Yi8loV~qIM, }3vpu0١j335bwr=X_Gh7[D}lEG(m!w?Ƈ$Nz5s=$Zltw%틭*2>WG"cI>/FwPy?8/'AMq9pUcM0uSw6wIR`9K{'\AioЙ%,lcҩ8/ ESwƻ%w/>4=U$U4!ЪXy>;\n\ B4:%#^/VgE}fOq.a"Ms{M ҤՎWxB~[ \i0u_5d Lvǐ95>Cv-4"g]Su2%c1.5PvgCb:7 ?g2yv;UlwLJ{;|Vq$y-𬦛s8 9.ax@DD3fײŁeMft( ަ:1D~_Vꓥ8ӧsdԉAGΟC|r3J}Hicؗ,6etT}eZ;LF=NGi8MK\˰kJe}ltQ{}xM_ 'Je3AM*0wʀ]+CՒdfj68J\*2JzF"s a4+0k mLsY>8ƔPX\Xk'EJ-O_%"!<n#Q?xcNWK.^c%]S4޺ dv'3/^8)XteY:<5n8d=3V _IJj`'>`gXzwzv 1)ɄsJ izpb8:D%K"m8ɗ2֕M]go$ IO1tY`z|EI@W =+_B!9E)/ȵ5F Otpg <&r>RZS;o6MMGj윿WLPJ\h_XqMZ!i yu%)a^&H7rV(?#Se)_ ;dWUBG'Q!k@?~{1pZ>4>2;$71kU(, ]_[Lݫ,%.13}5%PP Mr2I07EPю3ˤݸ#)鐗V]ĉsU'ݲ *_Nʎmj2-F&^ļjt]]5BnpqXx {ʗBn{.)&I3K@5Wĩ ӜcR^I6RzIN\q9 ce7TބC84ݟ5 ;;Ap65SVPP>vD2>OJdZj{RPWT`Z)SӢ]w&ˊilPЧ/ __ln)Rj \1?RzPq]ˬ?e;siddM BgSIr=bq5FNCJ~(BݤwIrrpV#>ׁ {]]g4r ,X e(ld_^$i /t܍P}i6=w؁0M~n=&<Z4E9LFxwoRןb~p%Y1[h19 4e_UCLGڣxC4J>UOB*O#۸qVFkΫ&IצCR=Ety0Y)ZqiCnӽ()"@/q1+G@bn!E40-t H}^GY%@{snِ}v^>l+Nxa,"\:⒪pϩ/1#&0{4hSSfcuӤ'l' hϝ<{}~԰biEƑUa Kbdqz$rm{ƳÛ١|cd[Ҥ @,|JMFJhПv `վp!7iW}CRe :+;,&s3^)N;nX#hV؎X99`KN>~h]xꄴl. J,٬GGڭjg5s+[^~} hntܶXVoỦ$DKqv*i= sn,.#HW&cʿ,H3v eDnb'wnsr(@Ƣm.2ꡫ\gK[11UW Z\f#"ũx<_ЦߐLG ;lد*_![P- :mpQ=^7yi}h8ºߴ&ش.>q:2=G01-9+>MllB`Fcvu>|vbK7ȑ+4Lhg?1Y{DD!2 d2S:4=Eu;[v5v([_*^rtqɄ)0~7?!8"{QzS?J66jZ3mYa"%l_7/^\ꯆ|,US!kڶH {Z-cH{ES.QN)wkߝC>!Uv6.I5sW㈄/$aj̞t)ChJB_voJtR@KMpҐ:/j"+hi]py}:%t#dkw6 "$kWD:bQQɎ Ư2Iʶ@]=X]PwG<& ;NDBx&kcrscm@${o,(¶a)p_a~cǴ5J{/yOǨfs""UNefĀPNe15eCK[m2y EOcc tKM6+=],?y_ol}mF$XAo7O9HIfo<8tkAuMD+[[ زV!yrk;]ZKKy* f]t9bPX3Vњ;2/T-\[0xOlB|yjv\AȺ4^=c[LGͯNGs$jYh'pDPT)d+*`s-Ajs*/ \B9q)pDAjf+8F^sFXUjT煀 `Ce45J.j{(řu+jMUx]2zڛ-|':w9eFΕͽC$䐻O&.Vu9?$%L,?tD6!Ԯ}'fVqϏ&^$E:ァ@ڠ6D)kticÏj@$]<(Vwҧ'ҍWku D.Vu&d>v)a;O>a&!5޽"? E[;V\m2K)laKR5na5N1s,䔪D~{e8BOƄ,i4{s[ƻ1tm_cyؓ9op_W ߳32K!i:oAׂVP48Nw&k^2/UQ5>pj N 8%9a$ڋiObC՜-'TRM5n',Wc~$ì\\ݴZ )˃Qbی"%'Ne1 W>5!VK7-N5?6vAtQ:dC;$\ |]- @^=l6nSƴOXAF$8h!qJ$qT)cyױAcTNmy>C&GKhlthř }R YI>&Fڕ؂ %Bmu^+qX6:)ݩc4;ȴ7L2vCȾr bчL{sȤ*S\_:ݱx% f'c)GX?+b֖ @jDE 4q [Im̍`,⟷6{T2HJ $w.CW7za B7d3;:cBD8T\tsmt+y1F uv_鑡 \V1M"TwID .s Uo]ѳ{OV![~~j ˈ@Pe vMf y rultJp*;i+mK\ gLOaxR/&Gt@AJ?Gl!v7勭`apͼ% O''hM0煕m -!A)5~6]P?'fsk"@Y7D|߅cpt%s\⛛B9 j_QY6PvCA;5J4ر5k6rRNFȺ4ts Q0Pmi?iҜ#8AD7I?ԕ,}nQ7\_Z'%U7.qHNXQY%gR RvͽBE'cG_ d?D/&^`⪑tvf7X]4@FP+zjlYXw1ݐr 6!I{&eM]e f _Y^ B9 />Mcs"D=JWC(}Gs-,/Uw!Ra^Oȯ?>qi.2`;5x zU%W? d*Yк@3C xo:Q+,WTS}Jغ^q$,#T1jQ5UUv kVỮM#vҔo=. NN=:¦$jW$ye3lѰuWR9Úٰ|_IPf{|Xǖ*ɨmVފb9HT\sm:qLcdX$)[h0Vt/zdq:SPkXEpOݛ=|d&,d$j>d7BSTg){XYQ/? ӄz5CoMt!hI2C'SYUK\#rwe,6u8Pȯ0/DCHBUU!ѵ=yKpGa_h7^77a.a[1'Iz_]}`eoЎZ Ƕ|2 ~Տd0Rn3'*K2;\j%YdKf"?ɒ#ม 3u~'%4ekc/@@KǷtіǝL ?nӶj7oM?5Qemه}\"Q[=7A&AB揁((ym?D;ydrUQ" 5Y/@-Bf.dpx\(̆;ditYtSvQχVwH8-X#ձ YWݳ7fDMWB KB_s7WkX AIyw|p)Ψ;d;UA xEq걚)'$=Yٰx}9<؞rOxRxJPQ+,v>ZaN]@?5ze2AQFH^t_dv@ߤޤ&g=NngEIg4)pj_s+O՟.LQs0ۄ!Z؃)6 }.a"`8:*|sڋ_iu=Š屘 ܐ)r/L:^gP+'?< F_2$W>r?`qWFsnw;QLxk(쨻@ӯ#[ŤPPHk!ތuLkk7!-b(#~ JnQd&f1DuMkszyJHg ˪),XW(6cϺFFL EmLJ/\9iF qfy"[ M]otpZ+mT#ou5A܇j=/*v@Z ;B߷J>YRwDWr2?&8/dkZREhUlɟM߀J]JAKwjv 09zKsvuA^'^۴n mL,JskS ^"$laK򐳤̲]Ƃ>˨Xgm㇜ + oiڠQSMyli@ݬ3as[f!"m2ǡLrQё`c/Ɣ} `ivQTgPm,7IJ:^p\mAISFeU<1^OL=D]LdswA-VFEpGu|-wZVգ `PX\: iR[5ѡ対`K٦L KUge Ol:GӾ>G/Xpj Pt5 棚<':C"c|}idXܫiNU,ո Nd9n^UL/&rG_ x@r.!qBv3A$%4TV܂KXRI lSe5}7EWiHH=|B MYݎ=fțjQ(1hzy.6 YkcFps KۮR3 Ҽ} /ฒ*;뻳- J/Y4q$ _|GޏDS5q}"T!ic/v+#N8wz֩r6KYL|DŽYy*]lZk~=bR*b>I]E[R(Y5.#WݕI7N(HakG4qruIsޏm,wnz/o4`S*DZNN8[ev޳]u{ F"Q^8[~Y DgA>YwI! N:˧Т7;)9um j3۷C4d8R;\I]m>C}dV9{!10)ZB %<MƓN_L#Tp_&053_P3=!dZB˥l f$iӱ~z/vf2Ӻ&N9E4]a5l@'B74j}[5f]oũ3A% 'wtԿOe21mztvcx ͡=]r^EV~nrl~$s3u)zBPJl6%>T@W.*V &v 5DpwxKn@„sSxn8d ,I<z .VYn޾N` ye5JExK+БB!^7u %VF-e;'o͵RA8,}&P 맠Hv쇨:5yIJ}8:u=dI/21̖=e%#?Rf(c WvhAhf7ReB!7 :?r|cD` %>h6ñL :.UNX}H02$8"Pv"ᢍa8e:5X0 S9ꓣOo=fQAR\d{W.?Lv~q|Ӡ3%VKX<ƺi >mFn_t2Ps-j,P!4oyA%grD9{'F.VYzN EתT$ "y'ۯrWpxZ1kPEoGҭChO`}ݔaݿ?eVCp^kAWbRLXv3c/oԐIګZ˺mjjf`fGCcT7<5 -({M寝_"S BR$6 PFN5)y,KY/[}PMiD+,v_}SWw2nK֔5^{,rp؟tZIPڽJĵ7I+SY;i2wNycUQ8cGW {@\?$ +!?BAXܶ\N=cG?Zì x$ڣ:8k d()9,>'+g#bTI8UɒIg`f"t V^vi]bxT=+I,哦\r0Qkq =ޫg*FWit?y8Z L@} u:_svQ h5 zaǙ&Se3fy<>({z2jC#."vXF|㌒rlxy4,O@ɉ|8%~ܬsf.khS{}NB'$~A?7`^KAOc{xF@mA@Dctnrx~݄8WiSѢ$w Urͮ-GJ]ף9. E\a=ItSHмj(}@o kܷ2H: 4T^\L|kqAdj417T΀X:RV@>KCDPN"0CmuOm߶'$}j]t~6-4zˆrkb0˦)DoXիGOC  E,8'nHǿׄѺ")w7~E~&1PSģS&_ O`/AbO\py ln,+{ `5 8JvyZڥ[Y%CyʭKPRL{}JB ˑeB=۟eeS(m>Үk]Ͼ "3wjYd*#j V<++NBBKhKIs, NYkR^ ;cQb0BNRk[Zg9}׮}zM;GM=4ADh* Dl $9UDMb5DtTW`0Y=:viv(ayՙ|])dCC#sJI+Gulºc6 Y(/qTftۭf,>x,e#

U-f9 3Y+r{BlZXI=LXHMd(fuV zd+wiQKǁj}ꖙRgp[!G?zG/‬GGGm$@sT@{#>axia[V6<5ul{I=$:(Q@?TG|LPI:pBt|E&V5Og%KU-e㏺y~}Z*G*BVxΑwpj'/'#=<5&|wKSCgMnB;fϲ17("wa4>j'Uٵ;- kA> +Q]1igr$>/Jy@ 'bHK*dzMi|6[ >'O]qXzHg/ŋ+ΩC;,6M,ۗ;4I€6f l^0nn}vZ~c,Út gϾ~Q 3H9Vl1!/aoFx8>^ l @[]!^OqrGV)g^%'4o}H?݀X&uh>W 7 xWkW0I%A\dZ)y)v*McZ#8&F"͝܌$=Дq`Z-#̍PQnq khl=ax\uG̦Qq%hUӠ'3K0Nя1 э<ԖtkIaTQ}4^QB^koJQ>$BsYW' X;vj5INsvPZX ;+7Cy M|5nձc =u-N>Z"(hkBno܃}5ju13&0+RXpϼl!N.r /5ݙY}[w4j×ӗ9ޕ(z;B .'{:!xü{UR<(1&zV䍽XSg>d ɑ)R ! X+'.H][_Td Vr A0&$5y[\`_bvY]jJ )?͊WmOv0MG__e(5A8WhKd}4}nگvS5aۮl̮` t9]mO}fJj.qc8v)7>=eY7#K+!q= ;N*Vm=h0M/ƗU-G2wlTزcس8p9+3!sMkM֝OT?Aа'ޑNܰb~]pZfKK(8L2?t$XIFlЩd5"x&`qnѪ5f:^g:&>{h.G5^VU"ٞx?-`OE@YًE~>x/6KeVDVýz,5˸CՏCs+̥ڌ7yvaXMǂfv1QbXP.GS>B*K MO^10dUy+해yĝ|ORijb%j~ g[+5n] cn9WԂG^f+Ճ8X2@u7hqܞLdPz쒶H~5GzCgq38ߍkl#FT5$H n]@5;F_[ -{ϓԄu*3xPVOiw/-A0 B5rp_p#)yT>~`:8H#jyIjt؀Bu%=fp ytLde"]Y`Y`cg2_l->^b*km"S{Td^=3 cR_gދx_@,ҪiT8@yW.{4~/#B`W%{*|b}{pz6$ WwԀp7 u\)+#oh0{yzhfH? B+,1NQ%>C룹)p;3BWH.m.gO 3>mvKQB~]VM'xU'Ag{_M2,7D< 2S7}t$ȳ?ߤ p 8հY!Y¿@7S?ڱxRVٸA9wAy^pzC^(ReY{ai8Gz vްh ;oi2)8p *FUt۫ǷOd<7Y5\:EGfڃLD\P0w φ;P/h^{.O.KkqPY+@H(؇:Ĭvlsq9{?3O=5qY۱xU?kp 7GUJrIȢoY&)KZ:#r/Aӟժ[3{Ք"\J`%'>aF7őw4A.x2gXAl]ȭ:aa?;̛$/Z_ }ލ"NE} 3 Jb49DK W\V-ؕ5i@?]x߆͑}TF vL{]{Ob$lb2bT]%=Ph.4Bed{pGbmڸ Tf7Ys RyPc|<ݍ>d(aCվpR)&,JF 4]Ʒ2\.t#Y~;[,e"0= r6bxbQBFVr]tdMלm Ҧ -7d]&H<׈IaLʼn:?$zu \JC NtZ&Nӊ7l̀߰@NҴIurޣg(!ȅNㆱMƊb !#Jb(yb YG6p GIu L?g77vtoBYFCfhWάR鋂Vx Z#>j˪rz)0`5 5]ddvxa=FcUELFC,k"ua" ԉgu.`j)M-Nb# &!w\!yY&*:-UQu |BLVj|O"ytkBUNw*;6$ 7^C@) 4pI>'mM [rpbT ,g_0#$ѨwK/JA>)fa6ߋbsgD ವ)$O"9OWIWwÏ9:ak0\m1 q}DB7t<-DC6aY輤v&R:9vE lgƇ}òev^*tFHdui9h# Yu,29]_3 ax -wAcYvfο>, hrhu >1[m#}mڕckR&:)y/9zdL:t)W=lzpw2ޗ/HXqfK^՜C,ͧwuTLd&6`@G5ҦPJvlBkCKPEyр pSoDhv90$]jz0|tlU֝V}cB17*}$U6N(3GL8HՑ"pX !lE!'9.ҽ sC6wy(59Qᾘ7d}kdfm&v>!1xʆ2n^H։@r(K_g΄\T,P^vYL>,㖆/~l!^4{O'*jP=׎tBJx/_mw`"p1d2Iy>0cnfl 8cUEn/Տ}qtЛcW -gF,>+u_MHgюk@j 3eS;oV2f׸I+=>å_t.JSȦ3I jF?+V:!4 = jE@RL>֔VmL&|ƀjʋ),LC*GclW]c:G?vxA+:k^L"9kgJ$_HS%b]pRİcQ]8b9^d!l*aupo\PKwSq悁,vwHrV)kRpD QduݍTi7]i8[V'`S.mP̹`$_ۡ&92)F-c{g6@|"9O΁ה}: ,,a yB9홚rx}%C fnmq1D-tт!!/"ӪϠƺ-/XZÆޅP' .wjR3 ]BơU\azߗYʑ4ڏv .UW`ia\ƅ@ _p!NL*̋nTej9JG>#GW;(/a]Q C3,|.4<#d;@LK%\Lb )_GSvRYnx#.]bȱu54hӘSA*50{ߤmܓ'4 -EoӀY~|2'{QHDz(\ "tbq69C̸*B6Zwj3J Ʈ$[Y\Z q\5¶fM8ﴩx\e ]]'$K*yXMM|o2ܗ&x YV4;Wi^I5xdO8D,7@,-qpKɪo2X`%mt6=[8CRC9Oơm988 IEWjJYᗀmMz>!r*vN!B(2bKa.t)ḉ-}*:*;,)En-.9.=IεۛH[t;sp;Jj D1s/x! #3bC#ٟEbݱԍx?6ESߝ$V0˗9Ʈpj"00u}qIsjJp1K[Ȕ#TO'x߱.(wbߤ'@9 CӽԣEv 7p6o @*>1jd.ɰZXE4qٗ#7w12;tA ތfEhj A>OZg6/铁b(9\2֪YϜ[㑟h'Umw]ݮ6W0=}6+w`@]tYz |Bξ 6H {>z W: bacY)XF)Zmr:L1 AoE6GKRnjnw)E{k9 խeog\lŏ@6*!K##{<wD(<#u)GV^Zt @ =>Ob/7*j"К5Íҩ*.p >&5?Vf _ Ƭk>p8}uqSv;n}kӎFTiJ^PPb@ 4:=[K r󑍑oĝ ,o#xyfy><HadES+XAQ u`x M%E< ʽ+%5_2'*1NXKv >61AWYLiFxk nUpdGR KMF1dS芧6~0-n}Da3 V\aI1FegpxWŞojꊆN4Yu"HP>h,Vką̰;dKI3o #<.d{ ` {wPI1Al}LW4Z j $RӪuJƗ :8ۢ#tu|w tٸ)u87q}=,g3j=zvN #2wm`$eR³cۚR8 7< 0JJ9&}fZ t;]K.(8Kbyg/idFU\/0rà|~@`$nH+KFGEv_c`S MH/s*N_Rw*=40_Ĵ6-8kg? Z\;>(-#YX w9n=ʮ0PgJY9 !dlA5٠.ȸwt!n8{2w "p!Uv3c ?&ԵT_Mꦁ-T_޶+뿅Roβ7C[ϠS!9ַ"<"'U} ؙXM|;%6oՂoSܟu"fR?se5b=4zh?5+_{op)<u-'cl )ƽ9'ɉ1ހ7lx(HQ(Z>='[uZEd152﫤_Ur-ǁ'QXX$:Bѱn@'NJ>_n02Qg[k~>Lk;YpAM fsR}nL՛J\JS|+ŁZjL+hSmF?^w`0*q%6u$XT$ad>Moaj3Ц7A,dJ% (Uu6 scq՗asDf;Nm;/H@=J~,:7CLDr3 .#z-==O'4`^KjYݽzN$=>@y,aԌ\mVBi&1ȱol׌ wN%&AiaDal0;ǻDr6z&~x\ٱkrvQ K$ ,8Sj6]{3:sIG>GIr} :E\߳? Aj3.h(Fc8`КY4y 5Íj@ISr>zX4[9 {ǑLa&夰361wPh bD'.WjJG; K3OcBQ59 ]mh GW½ zx_rUUXFV}LOD@,:hEG* vlagsy998@;x#?SXjPwfbh"+2ATl7p&lFK|/챤8`&&Ӳ q^&=xiPzҤ[σӠG ݔO$ 6j'CrG/C.adPa_5b4K[IaD:łs 8~_>3z!ߧA;)pO/XKR>_j܆D'g֌uط}Ҹ[j@Gɽ @ !5۳ul7z dOt2WGW}ØEiU@a?7'#7E"vQjSpB,g-m#bXpIz"kyzR9h{㼀 z{Ԥe){o`ɓaYF*8ڦL\w,2៍? s&uS!ks6զcÿYF #e^8CDSJ4;g޹fP5PM~ALKQ 54 [f71BSAx񥭝%Ű+T9xxa`FúcRkIz+,8%xdz  i&#OMxPvv4zi|-W?kdM0 uw\*N<@*1L4O?*VfگDSߴ8w5Xop0h$~lӡgŠ&Z ҾUfX)H+y=#0zU"x绒ĺ=-Er*xZ^w!ۂ/s'02LSa S>Z~R)6V2 `=pۧMvɇk(L*}GD\.w;8B'Yo:<mܹT4p屡9n 9؎ݱ6cp ǁK YE0h# uF}Soj0_Kb{q HR_@$Z2iȞ|RUuUîk]0R¦$|x#hW-)?NxgpihZf4U~„n#6mvdAgGKrĦC弰ZL%oϘ.njK81sP֋XŽzF\M H(SWs46kwŖ/Iʇx-3h jYOSɾ Ѱ+VЊσlnm/y∾U={E)_V-#ᄝ3c|iU{oә+ȳo<̍R8}TDW;wcr)d5 O.hPe{Wزo{[ 5'8z|Ra.؇ ^[.gf F92I]Ex$ƒ4}ooe"#Pmvv?#8]rq:!3Gk4tewR˄IEv>.h_o{#rB"dgyu-,ʜG{/lJ6Y5.="q|4B*_x.}VGdِ (uJ4WL"e<,ZVVa58at?=Ja@deҜN-T"z k25ןgշ<,)ыl=)13SrvyUk|bL]Y,xGw3_Ry:G=,]Y.u,{#Or݇9X RKP]bk`Lt0Nj:O}|Rqv QI&3&hw29%\ ^jl( tM5)9tg!mãG!Bo#TYViN. ][X/P8t>si2VQ @K l7 Ni| W_bi7S; w,y^j4D&yzMx{P7tr֛i,m2LiBL@yiXʮZJ1}L0@BKɼh}hrm1@BWr+](uMϏФ{QVm5CP~ 7[-e6=Q+r(iop:a}~~#@woEAzeնOP*CN|[O3wMt9&zOMYzel0_Y<0?J.h-ɱL-48*Y뺏baD]c<7Qh8 3e w(Dac@}_C#@3 ?(V 0^IK'GJrjc)YDI & w j])b D[v0;3];_8mfp*dj g”3|iA! r t:K_9zO`ΟqR9(OQOQ엫(.]KUșV J6`ٍVe nR܅21 o&d1| sbL`Ah MflbG <bɭ(h.F-YvΘnvT+lKA.wR|4 )!wz A:4N~l KX.ʆd{Ut?=E8 z~dOLxXmA(&ԒF+<n_ٚ܄#ŅnW9e+9(lǬb3Jjt: -&N)l+2e5\nbcSxZv70*-c8G֬(Wϣ <N|5ҒfzWq\e4j O߮sdzYC>놽N[/ˁWT'ˡMg"m5$V;i(>\*"v7 Vn@u#_e Qn%܊lkr=׬[e;6S`󨃴89nogoKgqCb ,?%{L)5rʬ1*26ۓ5jU8)P j\Tuëbo 1Bƿ]X=:~vwa쬪`U[v}!"0M&h "h^FKdvAbc <]|kbrW ŲyJ4GPPQ &;N\E5ӿU}ˤ]͋XaMpcеW6[̩^Z o[sb4J0Os*B{ng*f@whdMJC#amedpMZX8Ks􀠄YB>~j5\{ NdHWtغP: ,7dRy`6rX u}}DßT\ \ 1`pr $ ڊCy^ah&B''"Obv-~ڀ~WӪu(BZ{<7wi?rد $Wm@yrT_H6 ŭc @TX06_";S,6yl:K `piM˔yX?i}X0bs.R"k&uZycf` >_c j,(;%xo-[?3ʟ^DzPmhD8K5H\ 0ҧ֓n;[ ktkAXs{CRlF씣}n).&8A%rVkQ}hZ/, 7(<԰`csi*WK̕\*$DR5dn5҄ PKY׍3 b fVOh&'oDo5EX8y[UϚ 4yh)\TNxkCAۢceWh.;;11;>uHh@kLV6|DvHHuGK#oRr 2~{0߻1B動=AcLojz/b_ DF P ?)qc[F0QQ*%(ȃkvi<էփ`)+UT{b}f5sZ۽Ecvا)Q1> stream xuP[ -!hqwNpi]{p wwwww9gfrf_wEt?ke{P^V(jcHHPV@Z G}K3C <)=PZX`w4:L 154dn@F_@@ZY)A&B6nf&}v0w$#۩ @R omڸf kT`c %E%%@ hJ 04շ7t;\ }#׷(A)(D%'[[e%"FUUib*J4Ye/@_FfeD5EWpq!% /-= # %oeS3P6Ч=W@q%-Hj +) /2-Z Nw]EQD@XFRW~25ҁD qPD Rd j-vRߙЊ*JK*N&m_WL YZ^^`of :}kCP|NdhDwс!'{ߩ[eAP:Z.;N4f{Җ f{Kai^ee0.6{*@rw?7366]i-PB_  ځ)oڿ6o1o1"6c}K1 S{'?Ff:U.aml[ ߪ !_'%83t 1<#h\(~ OԢN WwY>D\Ffo|&@-#BG%h_N~H?:ZXll"Oܠ@*.F_KE m̬M@зwgM++o dur`c - v8"A z?@/1 f@]A v bW@]+A v?Į 'h3h-hoI Pɷ:Zș#{?G"4b93 ׿%,,%VV=uF JFvNQR6L D[kd &L3S7[S?VdfBZҴ#8FP~ph5jP [[}E_gak P;lQMFPv9Yt;FFPYQ4A rPI,La+(hGS{\Aw? @>%CVTm@P]{S@۟ A@)׽`K_Xffz}pd]Z 9ߴ}kAAWZeL̿13x2][t/r'{䎗@r~S\Ho$~mI%˳J.P' |]m/6RETSK)i%<(dL 2t(4vHUk9%%<*N x;ǷQ TSz9A$m;f۴=d^hFzzݐ2),^"ijF93(<8x#`벯ͼ=ڳNwzq9rYzdO8nF5P-yCNҲ_dwN m/jF8!Xq_ɉgP3b/_\oTaq$[gγfм9٬}y ;H瘂[_8 ?@VB0q6ÉLD@QrD{ *A=_gsִ6IŅ1%ux.TóY$Ua ~HNxޖ)f>.3:-iYr cv9. 8ڣj" =y8JP d:\%~Ya*7@[L<̚py-3#t~3'B@k<&Q>5nS)\ypyo:ogM:fh9A*Y6j"I]_f(Kɍt.xftJW a}݇"V)ڡ]ƍuKN^ 5L0.4wND|\&8$lg,O2Q娦B/HsȣޤisS\pRC8Aҷ.d,t~bÙ_.F#<) y;gwSFf[ܷ$ݥh|?N ʳEʝAV˳נ'`u(!ڳ.pxoOFdEZ2.k6v932n~LpӁxw8ˠU:=ћbf b ߶IB%$CqG#(ej^щfX`dr}O7!EI;+"1oZ rL2CpB{]sZUP4Zڻnr&m2GD;Ƌl%鴂iBӋr>2JZ6O91DWwpVYgƦsh h`[k>-sZ?4>@+48eQT,@ Q.o!GAhW?l! lnw%8NjSe>?c^c@rjY` ϖ`aIm#(8ce>n 8fW { ] һw)dq +_,=y) TC]7O!Ͼ:swFy\p &뿫rm~#eH,K*dU ]|ABрs!kAX҅!RlK59״cHäާ;r,rQ`M_`m#u)J꽚ṲRkJEDb6nW;`T䓿puÝ?lEl Vѕ4K`".O?n`={G}<E+ VЇ~ϤƂ+,hC>²I՝㔯/4#%K'|O)$eY[B26GZcHiK8eb0,E72-L1%͑%a|mz۪ߝV$SZaTܩ4: ~ii15,vrP'8񚃾 \t2RcKQ;ܷ7?~4}'t69ڤNpI~Mqh6W7H3cHƠx`b ۆh(!j(> E ֱ_%/M)5J/U*4 xd}oXAi՚eBؔ3ܿ<@@:?-= ]ڲK23qMŽk>t՗Tpe93}FP}F ͽN;Uq4&a}"ZS6kk%<.c^䧈_ʄp 5 r=n,+؟V'yl0.4e:ҽn$[ʦ|Xde؏O4,g2P`%jsXu瓽Slʔ&6SF+L<̖zt&n0e2*o!qlVt݄ުnkίtq9't'=zl) n_%A^9\5u+Ж{[˸L ֮݀i~ytTӑ cϟg4@?{u1>innԝ7VbN8t_fyGƵubw[gSPwj/d)) DYa48tCB UߙXy, Tm _=ru!5u ʎ]EWTKt>)GL'a_)͠Qxr\ ¡'ci(j!4<1wI|h,KNqZ6 '\Ry͔W&vVZ[NXGj+0I׽yˡiWte BmCo/xVfw6+"=^>Br"60MK.LXų^D+m^g6`*n=/=|(:^8' ,LA Ť" 971}|nkAÎ9n% N)S椥Ni?q?dj J $o Vwtć.;f /^fAu/yk>>Gi#)i74G M,/C̽)unktIk(NF z+g"f3[,'/5?(ctfMC DX|k- /+czuȦۥg QD(!BJ ;y˳+[J*gDV7eVeՊ,7څi38RvvyKڧwͲ6Lʯ*opsSwT09;y?δsj1a?ptنq.#O?;/siWc Iqg_IER= #"g܈|9۬ %Ļep!edyɎmG#g鋧M1!(vcI Ud\޾]ߏpH dw'(O%W@51s8YCӜ$bCʿ}K^Qm:5QZ \K(bD"j~poqGJbDMUDzҔ0un# {gd-7F g ъ! ?7'QΒm@rJSz?rj-`^uh( ϡ ^H7}ay% J|vkR}D͆ROj~KBluq)|ȂŰL_/Jz0F8q:B͐OIk59cys;:ZEWB/ް}ݍ2,Pw޽q|5Ρ胻Pr \ߛ}sI 'D^OdJfx0?Dz 8'r 6XM!ML*YkkINXvXTW'8UX+iu2iĵke\|ivFosنe D}!26퇦ѹrILpc5z]5t{IeѼV)&> qes1يp۩6:gkkm@PL{(QmYNCQJiXmRYsfV63+'⿮-BB{~fF2l y?(}$̨~&|vWBPg OL`_jwg ^2w[4Mc B[s] \;GCVL#&J0/8CMvB|Ժk38Bb7cX1X-g Mʯa *s (顢gX8ܿT|diԡմ->-k6l'f56.2n+[Bqĩڴ^xZ?> e4Z+tD"trmm $CY8D@#" LPz-#||^.Kg$b'^jNfiNع(M:ʀ!-qFg0:7XwU1$r:8M%1ڴPX}XUکEDbRh~Y'@FBm^N;PB5Y#c0 6|>ʞ@'j)6b9o~L=vt-qKɣ6H[[5^ݛ:]+9U=O=/ӕī*~UpE 7 rx3);[+K20u ؟60I:-2r=%Ǽ{߾2!93*>E_1JszS}PrX~|ϣ:."Ԋ7!RzGc'7ٽ/ֈxmBj%߬w ͸p,4q{#l|/2pkȄH43Ro ~ @;x\6jR؀8)\v>WtV4‚>(GTGH}5dx18>l`zcsy058{2) 橚׬WRuo|Q!~*l%QɭzFWtskhqs 5l1iK0hj/'vgfBYL*O#-|&㽩P޾ˁo -2a .i4jd3+KFe"*c/#֐5~3sjEl}l0rptz$NB\&˞EE=( 'Sւ ds;ETS69!RK/7uR+t?Kl-,s9p&0uA* 0^r(hxD~)aMݾ%!-g%*%6)8QA-dOɐS$/`;[N텵f'.jx5 YTt2O3;,| HGn}y= #7*\ҟ}2&кfm:z_lJifm/;z'8x|2T-zxA_=] [ j|>8e d}+a$^F<_&[pH(j/xʑ*-#vr@b𰭳8lON`nsJ/)3~WNN~+^K\mpNedk/$mLFYߞQ15/I^sHJD+8ׂ ?Svs.7NG[NW%, eSL.ʫ#%q:aD3s>q2R}!iuқrkC ŗ!NPArJ]R\Li^,&~\2Z[a0d}QTlP*ZΏ1o}Bݺ Ў˶=G+'(aWcjzq,}^ɵl˶wByKil`;c:J$~숐B򝃈]†$*G>Cw-66*w}̌蘆{*#Zkf! ڽk7gup)8<."puTSⅫEyyJ%WfVuN/F(Z gT>'y?Ș8l\/z/zr}$96)v]ETV=.3dGz5 % qGڊ_4u1blb$i<?OyW& }kH`a)\X=tJ}dprNx^.RvzV^.d}^l֟0bRMj S8Cs9>Mލ/iJ+l#8 ,>z,:lz3O,LtBgq61DO+E ҈K hө:Af6F٣f`b`R"o`xARWgZΆ xo[quqhe\Tԓab`(,kyfcO x_[߃^݃(MJ& ;zN%aoKWl03]G $usp`~;PX) 6mie@tx 4.e|:tKy#TË F]XVٛ2aV47҈8kr\T-7D (oe:ALܼ2< RHy LvcT+"Y9J͛4܍gNom|1f9v]>TUmt,EѩA'uK@,0Tav-%%4MoDX+>Q Y" f+%В .GK 4 &la: me"C.N"MBz]qD(EΜdTfcL כrߏ98ҳa4>HZ;~liG;e ғkK m2%aP WSƧbO&4b́wIMan&3q:n0x)!D5WBuUBrocȇ@:cx04|RV!-WP~X35̫Rbee.eᨆGs[P mpgs|UpDj"v"q4G=CaqDy8Zٱ)v^pY()-+cp35+dlaO\%DB3qߊ9EBBiɽmuc;g&Nv"\&tˡ\tΘg$fV/؄e`de30/^>4ǭ7CҰ!I }\"?8CT@c}IWga='h:eDSFƼ"T{Ǘ\*feDL#CDPK%bڽ E||6z0Ί$9+Niϙ联Y8?+jMWBק{A¶o\c"fT.vU "L}q͌:t:&}?/+ Fml-pM.Q(exv8+`0?F,,Z! 7OO#bjƔw4MykeUjzح~+'-;'9[9vYs<:.M>W$TڦN FoVG\֧өf =6=7]QtI^m7Mݰv4epr-22u j^~qf4|]KA+bH-0!i6zx΄Ajm^gt$O:/_'+\K >}#؉RW^M V:>~BsU^I_))FP2S3#ZzHǟR<;#M69!.5? >ГmPNP>hk,z*5uQ1rW=ovraɌ/%C4] IǕ&ճFvIYHn4N(w q(*8V(񭤵/(ikRܖE^z1l=^KIr f]Oh*%O!K>B],Fz໽T_h1z q4謙_)2: JRJKHL5oRWt?]EX P(mkLP}GTOi1r &4/-8ô>9EguaV>k# x_({N3>ti99o3gM)!JU *0qABt)RG&J>}¬/M)<`Һ&.[5(e۰!=>aG',0%swY> 9{yƣ lj/CՉ7{4"42cl/~л1ÙR΀RYt^%C~Z^.U0jK;)~ѦT霃S~h|Yn>XFzRuaL,Ieإlz|̝5& AJZ|D @[hKIPճ+rLp9wpyl&lJkth0h7#vVU|~˞4xK.)M8-~CF4#Ekf>kH%rB@ A 8S&!ڠOͶ~WD]P+u[$IhKi<UMǹ'/8Y ЙQ4F1;l^=rzݮs KL6*; I`jW!17liʙ{~_!؟U͜=O=N@1Oyϕ]=b؀ã!(߷5(N嫛ߑZ߽FtU^VBɊ.hlsu[ ,Nq0=%tiry^Q?tkKžjoi NOY +6t#=;W_~](2xD; 3\{_RjwZtaAXB^!|Oc$*e7(rbKԉ}\!aCx ]O?>SAo:BK NpsF%3`3CXiIg+K-aVه0⵼ }o42Դ i(g+ݻJIvv׭~ZɟqP]u0,=rWñclD^#(A!}N_R{Ode +C $yg&u?xwqFfh_x.##m+t0:&Wa:켜RLu +3fGGs$B%d>|"iZSx#)1Љvӭx݁EunܹM}N;Ve3 ݣ]sɆu2c-4' N/RR=! rC짮>Z*ϧ>$]}37[zh8"\E~x8mb@P2/;n2~r9 '?0oƷZheB괘w,# VHU.^ lc|?jA&ȣ-k)SlKr̸cJfÙo7t}HXԖNu`"AШuw?;]LpSzSE&_ eowPLyJSBR.bWIuk/T(Ѵ9jQ̦ qiͩAA23OTwПI`7;{`{(_* 6_p%[K^Z;L? ce_#Bw`Q|0˛4Vk9׫j'}2J=-ny[%Xq%uO"htΠaDTNET~'o;&dD_0waߖSW(Z_R׆::$uLø<. ';sꏣRpf Pfd ӴznI O?Wj#i6DFANQ "nq)Z]d#u?9YuA|s!wen沧cʹK}JUMBd>AaK(z˚ ՚V$@w+gE׏v:ʀq^CNéC)oXJ,,Hyg#Pnrhg.x|txYAk*V6QgΖ\Z }Tb2]W"&SjƉ^C1kWc D@k!xWYHXzHTz  Rʵ?n|ɉL} Ƹ@ϼ̝Ig.iyuO{~(Չq{hď6j\! Vby -  y7~%2G_ou<`b`员E9tJ+MzN=S4@?LUT`O_-'@: $SBE#mLc#eǷO_]jn9٠,cM.fB6!+VK'ӱ~El=wmy;yA!ҥ˳Uk|mae`&*ܳ|Ō=Ќf_4uTPO."$0'N?Ÿ QFykr݂J[)fxÝc0zC6 u~ /`TDSHd\* `ø;|0ICɝ5)d8L(ƶmvl۶m۶mضձm<͟8kZUGׅ d>ZϦՊۨ95o(=SP&xz/yS e.;ی[mUե na3{RX/5?ednPOϧLTSXG-!pvq^P,itOFʪ(g 5n);W-y;YuAVʓ]w˪5`!g8lVU!=9Bŏuب産6\N9 S\+-Uh FBUsJ5K rb6fe]X>t  vȕ<a9VHw2gWQD 6p+lΜVsQ 9ix)ˬlI0=% dO=LUT  fe-*W.0>oͅb\$ 7Dmv0L  _ ?Jn5o(s s% 38)p_`i4 x;WI 苿.!.8f?Fp:UGIߐ8dS#}r1Cf'*Llݐ33]8x:TC{ 2[ * #j+}ьbn,-{\4-P@nLK'ܮJ^I~3̯-$1mۜ 1?2ӾAID#Rо1}LMмZmR-/ۂ\kB#I޴"Rxbf\.A``5XV}WE 8`WQeIM JRv9 Y4K|ܴ(B;Z\R:P Ƹ)>I'Ʌ2!.H&GH@և4~-08uBM ?ꂙ? PYdDS<c0 vifSY=b) 3+-w/jD:'es&O(yǔ1~i<vS 'Vx7`'E[HQ*Ff_\i;!ڤ=j@JH7fg뫁n|ִBJj>]2j"^FX`~`9fϢ){*cƭ~jMAn1uL{(chiN:ssH6OL z)ZNP,"`]֪"~rQ7 sAT69ҷ*-K7NO,lUuXϜIK ͼgNxp ẟW:9,Q]|{1&\EFA^pvkvaŀ$C )9dOOѴ ?.Jdjt&n+4Bٷl3XNDjhN<ldT6uIh6PR &*'wq3WN_Υm R~{99-eNV2txDOawi.2cy4̊?V̒'ctAOl{ip"jAuZnñPhlOpvj8_"1T,ܠ$$&C\' 8W"O3xD$alFƘ4&,}wtv`ڟ߷%#Ӟ:O5KQaS |޿"|!Q|>3؏:<%ZO 7xS1GltJ' %0RH%6 6~Vń%ĺʪ<&SPP"5c8UPapj}㫪*;Dawx`z9 cGˇ m̮kZ=@ڇ>>l?]ǡik^Hґ3nB a= \C*[\D!& ^ -?go^ןd36``͙N .f7X'avJ%WM͝( ;ǧ|W4A __d&œq'[8;s"g^  NXm$ p41)$|^{WLxK~阒{ͬ#93UmEx\5 ?)u`9h|EN?i`dGӌr;kӀ@<HteC6.akSKL〕%9JnCzq"hDL)V1Kݵr jfC/{ 5dm,Ο" upف,&8zJxLqSkhaͻr| >9hyS`Kѫh^w(#FJ ,ʃ]繖m-%Ɏ7Z"։oك(ԀȢe EɰӳM(~+D伯0a:]D£$o}$0|fR48DGǹaʄzX~OSg[5hJY{vMv6'V/>o~b.ᑷzJ7ߜVЦFи(MO'!1|13KivuZU5j -~\,mz n.^[]exۊl4m\ԁPbIl5֦?2;z(3tZ4@dc,#}JtR.mc#s^Mz8C$JMlJ8ADhUR? ^/"VĢ7;yKܦ`ޯmN0۬ݰ>C@S"gCӥ liA0*;Kw`^ocq6TKԓ(n]<\|"}GJ."5f=%虋kebR6qe>}-D{& }nycP`tLjcE?[Y"mzCU}ca!܇ջvHThPm ]́IKN"^L1BU.{LITգI/Fxgk㑊_; Lզ6Nc/OܨMUDUS0JJEryU/ʧ؍LnX/0tNQaaAT<ɒJԜq o4x9Дܫx \YG]YL~}jD[wfosPҶqfkGxqO׭"6* шm t4R%$;^ )5wC$ YSßA?NEg(cռg;8VAvrJ'=.*Ά,%N6Xi+o]% QwoӊԖ,A<բl@xȑv.3pyOdgF]hoK1XϼN| |2C( ML%4U@E_&߳R2v@BKV{nrAթB"S n*=P$dH ?zNoRkbR{\>M WE4˥Ķ2% OT&&W4+|^:g}UN z> 1i@dv'/5xBzgִ]۾be1.:T.t{"SHiAM{~ \w qi$YuB (-l Z՛7!ߣףԧc|18Zmtޡ!Q`A4wn{ y7h}i.Ki)&Y,0Q8[@n+]pYѺ!iu8'f,?!OOU,gL{mG-X;(50slgUu"@+EDf[0&_rK.kzRV Q$vX͛ZwU½|BLpA7ΗPUlXaqQ14 6r;cKK[Qeki2 =g)tș`G_9hlP O8TJʔd/B>apb+"#.X`$DzӋ]]]vD PTWjsu{-y2T({cInij,-e{ B<#_F%>s:.Ll B;`Y93ӵBvbCÏ;$:>(( &"d ܙW?wƧ&d8|mܗri%:!ɳ腢Z;*og&UY17^yF3D0f(zta!^:FN^ ^^16\i2,}\Zi`/; Z dҏ=Pdp^E`@n=MTvZC!l)$Fף1VՆ&T5}0 DVO#Dbw.:"4va5t{͗؎`vAp $+m.,6H 'eYGoze֘H-n7F)»*RJx2Nr=EBprMa9:'а[&?oFu_VHV'|3€_W60uV-Iw161ڌ}!l=vi y-ys:->O9&?kfÓ!]T42u%\%Lg"l)6lEӍ%A%zGw  6+Jvd3&k:EuxgW^گR՗˛^(N T;Z@ 6gyO f3“EJ M92LQKTEv!EqQ0viRKrRjr ew3\C~, xYl^W'%ƹoQx;K=6&2v |."+[M9ЀE`:aHZZWuHdnp6UYȵmI叽moiI@o#AOL4Y 92WE #OJ濂-b]TjEf p5Zz(I/^CZlRfqG! _;,.ލaCdgkD@tCcT_(#ȕkl&-/2ųKs26wĈeLOGD4t@/r!Z{QF2NJm.y*nHM6H#B+$ !2o) p=xH֑um aմe$ٍ,@JϻOʣKyKd"3tzqG(U%SL\`qk3ʶ-$‚y񌇿^Ǒ.؊Lƍ-+]CQU϶KRנ}u' bh0La/nisXd2|i,sd ]- -,MîTNff XFjK猫:/Y8ے9 ^չ2ϯ|EO }ɾfx9S3z_~b|x'SxRq;(uN/Ŋ3dF nU83:B18\*x @#6 `H(8s`o\Ə> 4Nշނ#mAPJJnvKXL ;1!Z]Yz:cLpJx(YFОu!NW,.V?xjdHFؘA$`nnض Ŵ%t`feu~c$VRZl/#i8|A^iahD8]Ꮤu8c-45IhAwIGKf!V8y;XzL(] ;[Q(ChgP\N_V%cQ@l"*p9,$L4GhAбohL LBw%Q%QiF+ ,pSTm _t-秋dI Ow+Eż)!isͥKڣu T/,}eTeܷ҇MAqEMv^iUŊON,Mj'K.UFRp7SعO cV肗yIjI0̓g8dI2I*'ȩb\RWaaj?V" };l4)2rQ%?XEpDB4g>{iJSQG KZˆ"A [A5P)a䋖N/;Aި}mϱ W 4RV,Akחz;E* I|֙;%dmx}W cMhl)kg~n,HU2K![ix^G_Ouh3s\Y̎ӟVx{ 氇ַBEeO?6 LAA."p3.\s+2qOԆ5K}WfWpbB1jVF!X꼚B-5z?|Li[sYr1jVkk{nȩ.TV ;9qJ"pyMmqRC:l~#.0+ɝ?~h~ObCȰIuŻQ):Nd*9YGOg=fp KO'7$L]w\lmi g˵ZS\k9}'$/m~A VWWz>'H*>6^0>q78#:.ԧSx@B8=)[Rvdg'qSY9co?b5@SpG( '_'̜iГn, :~{Y  ^Hѿ}X}YSeR.!Nm#z-$$7Z=%PVBh(nkh謐η RRVi$c7[]|_n]y٪gOс{KReI鳑smmRIok*n W0vhct^oe=6CwgA T?=օe`.B]Jbݳ} JfwE ,"6D Eu 4XNm(<{r{+ yhI8xw |*zaEz&Rit@:,G0q[rz(Z~!/6V)>BF̶RpMNzomGˉuͣWO0\] 5@oOjg’X3ϼ.%fVt jkEl]r G7zUey; 3Vй/m%36'Ml IK2/i}I"%V~uMāus7 k+ޏ`*nUG@}j?\yFP ItSgh|.db#j/bZtK4Ή/-z.DUIAn֡1V&|Ȑ=A1H= +y̚cͨf5GH!Ж6疋-[r[?!%^rc=A3,ޖ )_6\;H r\w@p򯞹ŋPz&Pyr7!|ËFala R:!?c98P["&oSuSRm6lX.ZF4=VG@ u'PZ9E)ѰŖTJ*2(i(d6E7e_%1n汓yQTIS:s2e4QXFh J**d ;!87L..!7ް4@LKoU2Et${S%4[8ĝ{&:Tֵ69s#%u+n;;S#SAZ[h| -XŨ\1❼#;+/.q0jE7Q4H]5%y%(jG/N/Hyx;w սyhԴPEHHgeuQ|Z>!Ss/NiV{b j9{IY<@.n?9) K"|"M*z0-D^ix!ezՏ'k1ٜ\I$.;B&~N]gDiQX*{} &ļ'\7yCrWcOt !/pdG)wVٵiE[nuw 'E J*ue>xq2PU'8UP2149 kC`W5_g' MםۙV3d*|º^/{緲x \"Vzqk.Д@oN endstream endobj 1494 0 obj << /Type /ObjStm /N 100 /First 1013 /Length 3881 /Filter /FlateDecode >> stream x\[s~ǓJy~Re @ŐPw_4pVk:ci5iglCy2;8+w,\ hSܢ0P6TӈDiOz:}{uGjCQ]plf3qQU#QmA$h_zS t1?/<Ɵ޽ӊݖS,Ll`LY׋qjt2=Ē,TTӇ| Qĩ:Mt@VR7\nȝcݙuC &7lnr#S̔e,3e)LYf2S̔U2e)LYe*SV=]VS[w"="wa<8d*neBWu2c]vvDGֺme^k/9<#A<"x@`!/P A)^_pu6+:Z0yÄύrÄD@Ekc]SuT]gmWȰ9`NU}"ؼW)X}"b)@8, 6Ua@SR#D@fa) Da@SEy5qWr~T%7`j|H]R/:APMgh[/=}ҕ\w5AR&m. } MOX%` 4׌ۑ +5mzޭJN;3Ji1ܴG'Ϳ@J- 50 q)ʡa ?~.U.\Qsv;t_fΕ|Dr]z=W}gv>"&xD Y{ȟ5!~[K珱OCoh֘r'ӏ+얚I RQsҬ2_ ͳ ׂj"µ@(M|Qmt[Y(u1;v`\s’Kq?ٖx{{֝ylˤ "CQ;1ƌZ+@ݢ^6]l*f%w[R]Sr=k}]5>Ӹ*t_ _-;ߺZyMɖMj]فY=4s t潉Fs֢dI}uݷ6ѶU ݷuNfLnKcZV 5tӈ Ze'}e[bfk}gul{ZWH!H푗lm&HmBer\拪dto|GV翬F>+ΖTnׯ DQczUE3|PNW NswΦ%UyqZQFK9-H/>Z,&.m*_^'uaMW1Z[u\7;7Yы<r1_ޞOOjH*Z5fSխ5Bifn6K<~ ?g9_c?c>O3S^Dѿ)7%7_/?[Uy9oO\.dV9%,)?j>Z%_Ad9ė|Q|לzvZ._DKUVbB&+ԈPVVRT+U * Vܷ~oe㡢cd^ 3xe鶵pc{\f29Ø@lx6{Xɴ2m(V:zxᎂ#'~Ķ\[W)Wr7?Nh9LVi&xVhOq-tZt2h^tR.dq /13f,VKlʋ*x] V99#+'ݴ%UXHTa{ yŤa7?^~}{3uE,ߦ*?jwt-D-dw#ݟN㶷ѻ?6=I|_VШەN2Z"] koJMF_ӥ[r endstream endobj 1567 0 obj << /Length1 1610 /Length2 6267 /Length3 0 /Length 7210 /Filter /FlateDecode >> stream x}vw8mHDl75+"gm-jԬ*5jSTQ*jԪG5Z~~wy:p7TGTPCB@..% #4L @A2}L'O$IJKK{8 9$h ^a(OL)/p0y^7obq"`pG'4Gfr_ *"]a&*fGCho r#`HOQ"@P . bșgȞ5}%sèO='@ `_*kjYjg9UP=K (ď q@! bB ј:DY@Xo$ ָBa+$IWgt&WH l/CE7w|X%0VU & aAE˟ _aW /@ fU7 u-xUws`C08 0z8; Fjk5f@?ʏ `̌"E"`ܘܯܘ\w p9\@L?V__35fyp5E0x\-# &zM-WӈczVqL'5F!OWӵ26W1N(صMbFC %`jx], ?wD]#5s1O5wEC>@2atNCt h(l?_ ) qBRR z0{Ench`0(*\|R#2H0h#3M69?U52̷s2kՖp.MKk 'wzcG29K.Q&PZYrMl]\-f!pSNcF+l<6͹7JAYfxk ƉAt*JB:)!v+'Eb[>FhO+[P 8{/CM[KzԶSqݠf+UN} B u߶W ~pZ65RG{ICzQͶO H>!2d,e` )\'4K ZUnzA ׂ,wr,.1EOþO9/%da1b. m(j_}9e*BSD=NQloPEo_8FV =Ru!?2>4d67^x4魝g'L_WT;FkL&?.mҐHL޴^-{ҽmg*:e 5,e}Ēc _tϝ[}ܓHo*6l}Ǣu+Dd7 E=<$$/VP7T,NwJxjwXj, U2O_#90p-AeqſzH~^@ٻW"9MA\)=5zl":@:K7.6Kr ҫ2*jپY3ѣ*|'X=*$[xK_5qL3ͼ, p/ִ@Yev__T) h;0( #oIAj~G2ʴ1a'L%:ÁRI{p6-P3 KXеYc69|5f@`#75)!Z%ImRC^'wm ;޲ @27+5Ye ڎǦ>t&aZBЏ߆ RӪCa&e z3q{w r 40J吚K|8-J?GnMRJ\w4IBgE蟲d?3LzWy(ҨLuP7[_ ,@YX廋Y~UvKZ*ȩP샟s+bm#t8ΪNL%P*#icU<)ە?Q:¹[R 'alvp(B -P}ޠw+< yۀ}*ͻID ec_>B h0 k~k 35u%濔nGVŦh2 947_=e-т> )ш*PdOQz3rnz+z)@U~fE53'~.,r.BkpM`jHq2a׹ݎl1O^j% HL7Nz3]PYQWUP3O08nY·0Tsj|S{,0atAjv̆*m]O5]?_fAt݊%mJN輫L\#+vnZb; ژi+=1/BMnƤOĻOGwfqzA~^F?fzIcu&Au;* f I&.8`_󇀤F錓soǭƵ$c5 G2M@q?Wli뫻"(8hqv yւvﱼI}k!soMR+Z}vՒoIa~;x-ϑBeYpeܛs1)޷RO͏,5Ǻ HL<=K(SbbD$4]2_[>b8fA9'-UIhHD۫grտ(Ylmt7(,.:'cli9Qcw7ΰfBl"F=>ݻW=|~ZKvIg7w%"ďtBZz7w=-ߢ :Q v|u%; W!+ߝӐH~:ˏ :9̠¢h[ե.peXd1gT;uN[>u>yp:ߚ&F+DZa@cl X6q \AiΞ]Ol˛8wVUm ^XIߙ9%~'PUZ<:3~A]W{} >Sg&x77/=9Se*ԦDtjV(@8!{"Od/8Mx>F?(N̢;y" )[i튫Pl -3fⳢU~˸/Qb${՟~JMO1#7l[z\$$psRY)jS)즏K8=N-}<; ~urdY^9&^N/ ˼ByBrʍ͗П*Jtc;8g_-U-R~i蒶D-}]YKWߨfBz,/dB֙ݧBh/};?f)yM6v$٭7>';Ejeb"GJءeO2Zj%,H>2Ik?ۓDـGǔ@S];,Mɪ2C%I塋[9󢏬eit2JbwX_PY :%XkbEui+,MW~E_H@q1ޝ};Yh)!M_#Z鱱όGn[v38y Ptatd EVHAACӭ'0wűbiہ *b^ېgL U"^86rC$ wA 6[4aLՕYb 7soG|GPn[G #o[wfFLtҐvD00:"2 Gyk]h< ;S&SB;#\R6d'ducח^(tPAf1#{ޖV2+ʙ0f*]U j^}m ~)6EXb /Ku{$#z#kQt5んn 7s9 lP<4'WpCWKL)zr^"*Ws:sm~МduဠgSiZ~9o%TsE$K q^>9SdU}22) :.AǞGq_|fdZdK@ ,0w؋~{2 daPzQ|2gsx<҆Tj6 fW7SiVZʸyAG&7񻅯o|]3~%pdb0\WVvk ܝ/&$TIL/#j\sb8A&*I!gA%[@_#Q4/cWnH ,@ߕV{7]& Z"FA jy] ۖ|3?tr+J9 .x( >?U,Z$qɗR` ~əa88>>xuw2v/SBcR_yx@΂9L.7FDMK b==/ v! O_i{nS&PoO['f Nc%ntnL4U0E(DގAX2vC/(~*&(%VVy*^c]4add3y,v˝ZYNxhϽjAlϨ-L,R'vA YwvMxmHo<x}kl]U'gQf)Qr3:FV;m6ኴ|~~N%zdɞ5V&_Z`@M4  -UUoWe#U;y^+Oh-ҌjՐ|֌rhtWd>pc`ケvQC~KB5sN~@H.u|2hk19Bix<ٝ 'xEKd5[gj#'II^,h~-a]5v6&&T3( OMBeʼn@&&5'`B=>(|Rs<5[5أ6~{U,- A u<^Ddj[ƌěec'q>:5t^A櫕|peWS|F8֟}]6q⩀a.oeE| gwκeuתnY8G\#!!Fy0v2@'9v=t۬@G.$r Ι2(EE-~ oߥtR>nY@3ɚyIFqY˞64@M.R7ArN;\xSiU{iu(kOdκfQ֐y ӐQ˞˝: Zʾ%agbjyAH?=i_]3w`k>BaynKowZ.T,s5I GnV9AWwnFLyT\q) ,|6E{b|Bd7b$GrDq(opD}.k9I]kƁм,w"Zgݹ{:f~Lף7tlӦwЙV^gاBhjcTڈ?{WqF౨$@g +zZ$},<_h[^*.^<]v)Mw)Ķgyg_4w*S  Óo^Dsܷ4&4ƑmAW۷Ss`/ i& |ḈA<A,5t];%I8ߢJxCQMߐ)Ol^W4%oݬxB.iߕwf}9hBn 2%ܴqa>o1&Xqϔ?!5u7WmYSR+?(H&^gjR`Q`kMrQTvɉ4H]?bII\>XPBo$+?QD%( V(%|%ʿ=9Wyٲ&xܯrH{mf~[i\t endstream endobj 1570 0 obj << /Length1 2277 /Length2 10117 /Length3 0 /Length 11362 /Filter /FlateDecode >> stream x}wuXj6"(KtwHww. ,tw-ݝ҂t7H÷g{إ QTy'l1Cl130] LH #G0潑#0r"E9h&6 ''  t6HK٘BMls;@EiВ&+7 JqY? #Zdv_(lcf0m;]A&`G9 Ydo)B%cfb9hirp%q"CzYCo># _le2Z¢Jt/ b1sܐP`] Wh 6Gb{FFߦQ qE08O(XO(OE A>!('"\A\P.OE A(?!('\ԞrxBP.OE 4B#m{:bu[ NؙY8p[6;O)'06Z:X9Tc{# d?f?Y6[@ء+1k'~A'X@e 0U) @X!5T!ӧ,Si!N=~ߥ?C<dGB[{ 0Vo'?T?:b?1>mc4ӱt8!j SP`cmg8h'.Vh5? h w(mUly8Aa濱=6q43Dz]u'&"qx}kW13;4_q@'{Vr7a/6CtDy-*ʅ]m /~aw0EH14)LP-0>O{x5ۑ/#ֽ&r. Dd3LqIPN*Vc.ut3aK{` N!ֺEB8i*6zIv/7g@/-LCJG6SbKRצ"hrX$8NBVF|cX=Wm!ms3JlL#UãauUU+?΂6?ޠ {}Xbl-cn`\3ͭziV^ųBKϼΧr%B3.tV j02:݉Vy#f-b_ -6:X9&ꨟk΢xpWaDFpr @e@# Ǥs x! 9i?sUim\TT zl7+la%+/a* F=i2lM{]IP+ñKD}9)KuFAM&l=Lp:-Rx#yF"l=W2Q$rC/F n<?$FbK U|EHv;4W3; 9:XѶ[jIqzaxa W/m<_%br_^A:WK 9nNF ӵjµ*"%+$KVQfDF@"G5ね14~/ZTp.)?F ;tZQ/ AT^o'k ƆtwWI~5FP!jWGl|vls)DOQQ p{ЖJ~J,{+d_Q,Ws$G*W؈p~7pF y+p5?#>`E$bn0\F1eCpi[U|1DgK:YAnRgڍWR/XH5E.zD@EE2:i aI1ZvX2rdt%UʪeR~R)KZ~nw3!`:*i>6g b_NǒawJ%f?T 8²a:6ҍW,~4F 7H&U7K\":x^[`2g6AfsE5TwƄzicͫ"WőpaՊZa'K<=RaG߆@ꠄ&̺O^11Z0$QU}{*gkۖF)1)]<ɳϚptlb1YvGnCF! wǮ33+*˳]lSN*fZȅW~nّp+S?mb׏S dCMbr q6}E80f"z HJ*XePQu/ +b Expfv^Ɗ<8pQG(NBKcͩ` )@Aǒ>'˲T櫱ÓHŻ${ $t-3Ùދ}ӒbzV01!gNy;7!m"b=ɗ7=jl q噘 ŗ.zA6>i.r[\ꯘY#߶{`][`@eTco(M>l_.lbMBِ! tfv9K?6vLfK`8p~[Y:D!?JS,*n|bw [9ibC=hiD'~M U;! r8bs|q@),u:$C/x|INf%M&?SͻioiYr9|Ri-f67oĆvdj6`rN1>h'RJ)^ՑOc P;Eg_r9 Nz!;M|D֥gx|ӡoo*@2\ˋ,痓)JߣB&4o,$E~`#JU xxhZC5oG]UQE삀|;3T@^6 ݽ׷k(fw)^8\iqWgg|wGc?_,q^ßOiQLݵP 5(ڝtqy´P]Zv&8 No$6֘~VG#"#v _Qψh'y& `{; dv>0GjwZ:Uyw>ؒ9\e{gTujK'Tg.EOs:2mfRyݢW^jO#8K_ ͺbͼg} H7ǣo[훟E(SzZ*ɘs9R'IR^_}q}R4_%Ɲ9Iu{=k|j C,ft#[#!|^*r*}:#i*r]/|\ʕ?N fxn~Jx]p&L9.꾻ſہU>H/B!:;4-/eR:~ȗ+߽|.a5-`g+E^nsgJ4RNs<9ץLRa}1"e׷-Mz]I1Lgs?BUx=ؿ!{p.HU9 Py{_㳌قF w%acGa.g$%$ZAҥvoX(~tPjg5@ޛ*kce(zG2c3(J7 }L77+cXre4BSR@uIݙJp[MJJ"x֦ʗuα ,5nќ_i%106z g¿G|z(փ72.v& ZS9R+mRꅢ0Zn:;9Um> q`wg/ ڙBP[$ATCb I"yy94Zj$/>:.|밨jԒV3ق[DUo2ұs ^C'AD)<ۅ0mQMŤK|pJ =z:@.*ZI/ QU4\KWLiƌeMSXe/ U9L,޼ؑfv _!%+óTV{6"t(ޤ\YShSF`[9 ҋE>j6fhI }UNHQ>UUSR,iQl,ҹnwi=RʼnJPSzϊjэE{},|@l.W_ёw>p*5(=ӳUf [1$ch 1GwZűgM#=Bd!v`$@VRp5hSx|dҗ5>.J|WL~r!ƴ p)bd2/#1%>X"D+F\zf$wՑG7A\\lI-~<\>t/>&X`P /r=eoKl fYx, /ʞN))J8|Zpė:4̘ p{eRKTi6c Q[?aR *cm*6VcL^9[uR~bÙt_deQ>q]eޙА98Ϫ UF,ْsh Yldno+ ބ{rAnqEo*1g"SC1;G%`nXʵr1v0ӯ‰.7ǣ{Xc#iC'LG, KJ\Q p߼3hm6 h ֕I*seGFq@Ӥ@zU6k{B?1q4MXɪWJ: ؝uoۃ)MuT㾚VWC-U.hϥt sy H qh7twZ=lchc!l^$_iPFbaF'ҟnxaZ`Ė֊Fog:UtkNۢ|j ()%Y N>;oB%v\.C?q*X!e{oꂾ$ow #_\y JVI1,OlZZ(.26|}U:qNq)j/|V9k-k,r;jӚre;3DT=/CVdSkż[/݌+*մN->XJ[Hxj,|N8P㼚6HQuq+@MڂP Q  Ѭi)V+)HΗH΢sLǰOB̪3ߖ:\EKoA%~j]?y (D"ʁk7d!X!:B_Yu$aw#7w;nWJю;/84;ZbC4ܦ-F=W76?|ͺ3]^(%+|r8/X'#ǒ]ijh]%'Kl7N ˛qt=E:i#Tgp]s tլ36T#&!4ܒ1QdU,B?}1. t\xTM`[=e{LvLJ'uD.gC-/c_w_jN&x)]HJ(;]1.k9ڻK>f-"%d-$`+z>Oy"8j01іF}h@i~c)u j]REM^ z9p6Y/p=@A!ȘY`Ep㢘'WU$]2$DժοDfvQ \4WLȜzWߐKrQHd;*鶕&4NջO [sC ~ _i瀅.IYw̻"ѱغfVU5i*qJ#|M_Y6ksL ![gH+"yL':w{߃4Ap7ZÇ:VQ& }w5e#w:YME>Gosi$-SR<r[D)goߡ7 #ERbQ5!6-3ON*0#3[/R.T-(wy P8 zYcvc#W#elUUZzI ݳ=K=%G:La<慾 % AC&?3Tϵ67no\[^jeIY3TAM([[L蝳n $ [6EL]RI ́oz9BKGIͫO 2J(Y̓ZN7N&Qݙ$owmӿ25æN>E"/Q?N}=Nh>+2ѻŨsvCK.k'7X܁-b8ߔt~BYxc3()LeqI*MGeѣUiѼmU* ~vkhy >qa#.Qշ+.7c`+wwu+A_RL}{X. 8:ͪa {Ґm6Q{M {=꼕O3C%j_-kv9NG$%E:+&X6?*&)QvvxwLBq^F^}a{Y I٘W28B̪HxkC8_F;T48z< .Dɖ}d;*o yZa2M|mzRÇ\T~wTpeV=;oLм]_JU2gO>N *9[ѷ)B.~3hY iB%{|Zڟ#P$|Kqȹ?qMq: UI[nnKQa;|ys/xVb6fVe靐 ^&~ },'&!#jd/Ȯr$(iƶx0,# 7onĹ[0x'P8įPl57nM&xi3;j]z fIoZBFK7lGq[V TL%OݽzN%r/<ۨ42T7 M+D?vUGf_/7Mڎ)I'La4fd [iʾ/1Cc\qzQ?dsoM'en)E& !UAFZsJ&|Q:4dۂfuW&ՏjgޙhLu1kg9ÿZzys_H.XO% 11%k瓆GBnxb;`fZ\m?ɞ|'hy1M0GY!ng1Aꭌλ:E]'wrjUdyʣ.}ERJʄVT GA?c8X; V$ \k&2ծ'K"a) g18DO,%؇TkpLv_a+BWN vDhGW l^Bsycu8-2W,\~#vEőuK<=ZCRafWŷa8@효?q ApH ~eB=-1׈]?J\eSlW ,.Jy/6pܳICTޭ&FʒnivPAN{O Imqs+<6pQCk-*^xSW2.iT|pi+ضx `BW 8L܃tp̴OE0눸ןO>w5V=}VhNZ^N+z]`Df !]3\m +(}=9E!=T@Dʯ*spBs᧣ϒfٞ{k;ʭM{Aκ@nV:]F.*n3*>˕d;[]B~My06o4ȜmyD`Ÿf#R9 F=M2lluheR9 ~bPL#/_/~nUT8v=jsoRQ\uMaޖrm?\A`=Իj%&P>5 U"˝! (9:tׁQvyq|j܅qhp;{Q:ZE]e%ȶ'Vj%%>T@DJ^K\Pn$wW5_Wݳ]%$JěWL1a%x.%#BpzN7!r4cFZ_9|km•::tͣuhMo[UV?sA4n"$A@v9qHMFQn)[adsa.+$ ]\wX1ւXܸ"'Y :iM ĵ=Gġj, IB.:5']qbF;PlsۉjH o,Q'q5B!y''Vj԰^Q3#NB qѓ~b~:) Hƍd#"u:1fCmn~7au";7599 ApHQ"_md&gu ׉X0}m"s^kMgJ =OעeY@WI"!:lE,ݶu%FO ~7`^y>ucƀB^ #pwU aA/ݒ endstream endobj 1572 0 obj << /Length1 2065 /Length2 8762 /Length3 0 /Length 9911 /Filter /FlateDecode >> stream x}uu\k6%,twwtw,.,Kw7(%) H#R<>kf暹>a[ (;7x90=`(XAaHGSae;#(LJu Av`v[O F QڂA.[윧s+W 鯺`twԁUZ8H]va>!. >@[MG m!. &wcܰ&ۧh zxz:*PN6tu0ٜ 0v ؁!g&m 8e S 80S$TxDNGTzDNGTyD0vGcWD0Ʈk>"#<"#i{D0vGc7xD0>0gYy@GX n3ⴶX'\< ? <r6@[k?¬ &1-?Lww 돩Òx8<bGbGM0PkXw=l?{ VcX3,w^ȚV c( 4p:n{>&\\{c/#g}`+(8O@>v<pzuF›}{FOl` ?aA 5[q]Ҷt,Ks2|դi_nZRN~pK愽hHm &(8E Ʉ1`.Rwn]c+6(soDt:jd1y驫B /eQ"f]` 25o#}%~|U?MA 6 ܓBwE5U7Ӗmb-,]]=jzj(cQ=ܽ尐uc*deD5Խ](Pd4;lelO<6 /LV7mS5M[9)A<Ǽlθ t ? - nNQ(w2ME/TT:$N+%scٸ$}mۓb/+7|8XQX):ID3X-6P[^Cdζ;J-(1>kdn{_#JڲiI:+E&-k$~$ּUwimx8'IY~Gn{(Ƅ^{Nf^74}Pu0БkK5KO!hHߔ bpVT_dָz=BA[,V(t}ZI19z_X^mԆ$a 3lX*.f /8x:mt솏uSלQ}0<5aªd$hm;1мLM袻yV)B2_JZX"gp!bsUpB.C_T֮ݞKP/vq-m(JnHIZS3 Ԁ3 z3O#?M4'Icd4gu}2O{'G֑Nw7R+Zjk&+xP/S&%&*YlHPM2gR} L:y* L vOO?g(&DE4vNT‹w0ED*|'U4O~K)Y߶+ۇWA?`*JpsnBjPY)5,xl)A-Ip6R4Nx^8편{m] y H&6w\' g8k^~߆@^:  @tn1!.4QI_"%Z.F X $b>s7$8r0;we{[. f#&2v7"-2/ўhAP21,-R_m2E[W|Vd>pdWz[]wn()bv9I9őOLe5dy3g1SA Kug.l)4i,|g6~@AؘtShZt˿÷& V=̡ $Y&j0Ewz0yf|mm|vپ"*D=,ya<[䯻~_d ݩqn[iz' zu/xRX{{\ɓE|UNO!2Eb"#P  E̚KyѴN8ar_$!i6f*vυB QaZY| cmoj%K92LFIHOJx}ftWsozRuM\8;볚邇Ly_:HlnH<¼p*d^N9 ~bz̮ lܙJձyy1Q3b㰨{0٬*v˘ZSE)>%'Z:K/2;C3$Vz[3=m.~pF-\s#(Y:_ }V+{Ant3G-3Į;Mk/MBuCDCCM[%VrQ;*$>3~2bv tnQH!%=,QJ 0JrpC<7עXrt"-DmV!NqBjZ&ڷd/m6HJ(+₴:,6Z.\*( ߅H3ubb_Sԕ[m)D)Jg[ [awԺAGP<|v@CfuWٚmcT8 |ίG I߉!j106O_j!*-H;,7 OŔEPq3Qۛ74KLDoqPSrUģPr2KީcOJ+a';莬R0@L&\&=t6k^K WPwPBiH>O7z2?-51TxLGrHlpNޖUpZkU giof7X WV+ja ܖO uCfVx~΄?XZF{5F* 3MUd};Mmѭ5rL".hΚN 7 [͈s">dWPW<YO$#1`RF jj//04T9GLj"<+@,hyqwsQ:g֘}͠;HPqORC/iu_sAڠh1\ ϒ|ƖUvvXS悝*V*3]`n<[_u}v=Bs1vR@S "p8l}]=7k/S՜b4hϒF Br7O [XΖ ;¸o\v DQ{NJtX"v\c-:MU2S2!/:Gn]ʍfuSLF^]=]-İ ;^9Fxi˙z]zLU# '_K9e(fǠ: aPT:)/jo;y1gjӑ5Ҕ 藫ܢpP(fe^j xB1HGEO-, b^('@:bTW~VOc'U;)u ReonMʾӤe'?\󆥚Uq&)w=B64؄2~E_nCsm6:/U4,zb`,7 6ui[PI^ʹ7sbGkd̈́/@-22ЎiG#^N)$p<>h0U;ּ .gTx}vyMR z)^?w/rH3ce4GqȴڦdiYP8pm?t8^bb1xBB9Hz{} DT W",F\_訏:@(=XO0\3 e=W̏-cDDWG)fő)%?H8/@y0F͎ܕ8^ZTez)y~F/K,⅘>iHJ3g3xrH"PoD`H4O8MȆ?+/8_,"I?`2XVh7bs>@<9#|7|d5}P:~8{E`)5Y juPbcUږ~%wBJ*9vVeD]mq_2\[8&V]u$o?(I U~da۝iX[ W'zSA2bnE 5EbWwˏms)v/lZI(DфbKC4:Ry!A'8MƧ# kJUGrbE=JYk]z<Srzϝ Ŭ,|w2CK. UN笤kBW~_Я,i5~'U\}=F0Ǒ#b"uF:me'tί gcʂ4*n%KY5K` Io_:g–RAgd/%}qa26O.T8r?|Rc>Jτ,G}Enϼ$,cwP!#I؂hY[3}\9W Oܸx98UY}+2Kbf@٨V.)S 5"#Ld۔e"\m){ހM-w"{>{,ic\eITL| bDZЧ[!hyymIW_ك| Two┈'B h#?tWЋdOR8ة.-2)*Ň (b{D^c(ó!XWlo-fϿ{s* 7~bѴǻڒf?nxƷ߇>Il5aKa^՟w؄:ׇt)Q`[.aӋ C\Bʥ֒N>ѭaz<ݺ-R3;ՙl7,@O@^w֓ߑ}X+i޽F7.5){RK!bY, ioQkތw1jkZd\s~H΅;G&E & ?'$A16!}`LkLb} PţtL'Y}sKjxDC6B=B%Ǎj zW/Є}KCJA}Xvh&}[ΡQH7TwR}s-`"^[4sG5>cuf~&Xj*k]{!:sbveI21 SfGLd[wBBv *8 X8ƕ#)Qw'gT|k ?]zo'K҂ ˱#J#^%GA<՟x}=ϬoU34[km.ZxXgk )g8.Z# Lf%˺ب6.4a=k4< h=)+MV2yA. Ȅp|e<@FRElr }Wc|ZWy6?j-A)]mH,~͐ -iY, jw͔%d2;T" W,ᵮo'̄RB :uRM$]9v_oՐd?V-/&i 19a{Hf׻Lr#"]ҦGCc) I]ڿ KwsL {%@c9Y5K4Bcf~ n&"|\zf̽dd5n- N!/遘+m\nzgܛXKTu[=bLzbl`}OIu¡vT?3|;*;TV (WeK>FyRw4'_ b֡'RljðbХ~j[rqf_XzaXb 695$*m(FL0^ K"[7]bA]Df.i y3<{dF4M؃ 2F}1oUP aLe|8kܼu " ;r{KD78ڈW\(ܵ g^UU,oFQ:zdp&SkR?[3ŧ!2l̪!ex?.=2SՌW-4#|ſ9ےiXKR|$iӋ3jK8dYyܗ Z)< KB#Ø}c| J˧QDSd[.v_O㠘%zsv4ChA7;4zzd+eFS,=O^~t0 YO::T $HQ~ˆ:EDvsqnt7ÆnɈ3"f@ kTl;hצ" JlTTrD[ʎ'DqrH *ʈ KS C\y{R)Cp|Xc>hc.W-Gv&MH%6MTNjFSO u4>$cO*ڦTSW|ua#񶟗Tz{RQiC w]QC{Cã@Bn#;/F3{!l6T ZUdᛔW+P QOB{M %"QӮua\I5ym[éWSJΒ{jeV)>?|F-V )b>Yƭ{ҽ?Qhc35l$Nʏ&e]pb2J⧖!=㍒%H6GUhC{AGr(Bg%C :Q6U*$33]K-|/$P^߉nNz UvfЏ.V*j1-?͒,P7dc'J= k_N >Rm:>ba1e ݒJ3=}S^~ N k{GNA-s u0 H e5KRzq 6x^南P}kv5Br]*=CLFy)- h#5P L96lF>Y`#u[tA9MhD̻<[Z9e)q]%Rd nkV̼4#֕-<a> yp9J VWJFD[Ɍ-ZMUB endstream endobj 1574 0 obj << /Length1 757 /Length2 725 /Length3 0 /Length 1253 /Filter /FlateDecode >> stream x}RkPWeAȠ"J)*!  cgHQc0 V7KXMva<i%DwGF+jATZ,,>upZjWL|^/U(d4pC#Қ4i&:@PpK0 ⑜ߨ|2#t$HRL Ii@E "Q4k&o1[ $L(ߨt@B )]*~JHRFD5zJF`-LTQH ktq,HsߨYOj) jm64u}#aםߴ-u%#Zȃ_-QZWp-cM :W!nS+tp65ld{UIIu_/J{ z `K7b%͉^GMnElR1.2y>?{(rz9@&&Pq[`.G]E9{^it{pRk!98xO}m̭xGp=wytԩTݜ33zc ld\l~P];ҬM[){|{W=m-g:BN\PӱxCwViwo~`ĖͯR̈JnnX,nuvDŽ ayNeחM_=vO2tbhMZOB剨nOK?X endstream endobj 1576 0 obj << /Length1 775 /Length2 772 /Length3 0 /Length 1317 /Filter /FlateDecode >> stream x}RkPW#UG )V^&"FI% V.l0B6,P["CAQAX(htR|0h-XjMNř|[$7T8FrH :~l᱆2űwd$ \CH Q*i52-#FtZɾ֑.t/+zI(wUV"gõh8J%=֡& eUOZFx+W|y&)r#P<[ͯr_W&؜=nn-wxW7: #7tnэ7f=3js4pinwQtȐi hy~T)rٰZq+84> stream xڭTy\6((ELE k ! &3y $*PTAPQWA(+KV}?;s012g_T-%q/0Pvk ##W (A$bx >,6a\q\ bI`3>YW@ByO<""#) E@ *Djn0 H !J(pP>)%@8v|QEi9LbRn#J?@ @IRXe %8uCDa'H/A$rVIB"6R0c0Η*JRb Dd"V4` !9"KPeR20DI`!B ŭΧ:b\+o}% DcbI*4*f 9fX<"Q6D13TcB98I&Oe'gYg+B/$`lj@ ŢB?@"T(+yXAEBT[1% mned(!0% R}Sڃ0Q U0XL8,0BXALYRgNHЪU?/YF(  2P.FVǃ `XرHƶJ?DOU)Ae id(RŗGqX1H$}4(`T"$Wg+@׸׊s~t RϚذU|&4KO>fVw#<^[.yq`^R ǛF^\0f#ͿqR2[Ҕ/ 3{sfY;af~ܟ.}õ_U/?ңQ7r$EHGUmw*r*]h8p{gZө5:'EZlf 6zGV}fӳjGmltڌV|2Q`'ϫw RwL;ͶVU+gd8U4t(+`Ke4,G'2XO2Sh\CGWt/Uv9wv=+Oe_:rH'M˳CT>ӫ׻#ԅ_nizˊ5O"i|7>پ%IVGf܉>y,<爇λm0 6_t Tٶ3ZUn D-0!BXwTrۺ9M]S NVu<8Hܣ!>="?~7mNCq{sZekWM lrw[館8^sSo2|6=`!ג=q[4ES*"5e_J4Mޕ/ssRSK|Jt%u{ $0mՕ,s_x9̯ ds;XGkj/gkkݞ>Zܖv댖vwlf$w z_vz]6bYS|cӖ'/8+T1 EdoXI"C*'>10}C/ ]' ܱd'%Ms-X6qJ/<5fWOXfoK읨u<:m7+{-hT emW ;Lg],cM;G3qa;Wmn9iZm4iq7mwgw O ߥHiv;쉶Fb6/ '%w6_֨tDaѥ l9[߲z6JQ rO n.9PZ,rҥoۋ78ssh7M6JS͋ jFQMͻRtmkykPc,/~ !^>OXxk|$c:/RetD?)F 9c:ŤOơsyb*)w8Q endstream endobj 1569 0 obj << /Type /ObjStm /N 100 /First 952 /Length 3111 /Filter /FlateDecode >> stream xZYs8~ׯn3ñL|ę%673~ hˎk7a@JeATj(aI?4Toᅵp+9Eq7DL ƅHDjAS+ q. q7r i ѠM7 $d9tddFB ,HKAA'R!I$' $rO(RIdER2 Vzd4U4wh@jH12pQpV)"@2) ( )C͈2P4gi P@iSᙻ3$7N*8$E+$0A} `i e ` U5>0LBPq>( !R23jp˭vQ`  /0 8 ´0`2gJ!VzEa`䤵*.OF!qUf-V*RW&$fH7CRP#.) HАo) I7bIW`xT 1:62Xa9 9>R7KpoI!$EºO?W%-̗8ÛÓ_(ݤ,,.#;$`A@Tư/#<.F~ܾI Ό{GF2ez"2'e:?E#,7$-?kc3'8ߦ"Y\<|/%Nq*+|}KUZk|L2]|&] 3[߲"oJ$ZՠK@^=NfɢwŒg$ݞ}$j5W-if*`dO/%VFG 2Z5-H҄`ܺ:T H˃g.ոp[a#OG:2~mLq68{}EM[ [Xؖ5Zݮ˚"Rֲ$Kٌ4`5J|iഩ˃2&Ͽj|,j6uVUo*p6׫ sߝw1Kl4r_]fp BFu;Wj5} h/ak'G\ݦUg:QՓ|Z`lUiU B}77;Z FXPŞTGsf~^%uӘ#+iz KLW{.1<BY/۟Yz0Y𯲀 OV@.]@ŭ&euBOԽ}<>()o|ʪ#7t(@*[+X8DaҸ]7;S?6jw>xx}>gŠ" E8mu]fwǧ}q^L@9{;wц{l;P<@1'lHjzMe3yhv(i<򛃳sO< QZt;3y&f|>8<F&/X|b V*+5̋6y\~>뺶6;[s6=ݯ?hn4.~t Xˡf z6p.LhOj6<&l!9jü4o[m-rE.~H6ʝFpt{5]v {9e*(%*õj_eP7n|kCoiD#2(W!c  YgĆX߶e ϛG Y&DanЖٝEA(([o` HS2Ơhb:Ʃ1eb IISSD!> Qx)ː+bM8F k2qA1#<'b.BRz!`SLE1cKOL|bSLE܏A9eQqrF`$ &6E)+..ca!noт6M*Grr`J`A  !*J 1WK߉V!a/q%a1 2`Vb˸Ec@G*nV[t}@&OGu dd3ΚMW,[8e۽VQ;r=5+g)  >,oy4a:]OM?V, 21yO\NTFxSYGLٵ !([T_\X٤2}j%[x%kՂ[-k%^y]mQ-юh.֧^ b/Z؋>E=Ğ}{>=oa/5|{žaj =ÞسAY {և={ZcO-YƞbOZ>I=>gӡ'm5Ozo|~؃ucDѵoMUl>86<~?ֺetKW_Ժ.EtZҥY^vthAǽ5z{^ak ^ثAU {݇W[UڪF[j½հ~V.(1*H>lٗYEAei+ubD'1Nwfשth6o%F%FԉzPW z5ޜj5b2}hm66mWƞuX6Ճ[h>i6^Oh/ėeUy> stream x}Yˊ%7 ߯^~00 !,z1 ȷʥ]ttsC%q 7%N\{®$*zBb&zP*KO˃ER!5"][Sz)&'E$IĨiԿ 0. eE?qťj P4EtzD$]4ԮJOEdyP億zKPŢ0Itq$(C$jĺ)0RN "N&P@',=4M YT&f ],%Ё@jTYG'9?P aZ *FdP]Pdh Tt6Dxybm婨.!9b7]?' \rPsIKr5\hhE~Gj+_j+UJ\]\gto1~l"Չ뗯߾_|7>%M,(DE}䒨y1Nd.;QB\lls9QNK.cam6gt4dl!s&cN)樕ۓ(& )gt$ ezG2v}m-bJ{ Q`mڅh7=W$GshtH"3Y:#$4 eIvHĉ8a"0+ Ak4b+7W] ^N(ےJFYUjf|jjխZ].r wx\rDehFJU1 o=bl%R%b[mm>(\6of!N>q=o'=GܓqO[qdӖ{rS=q=Fc 8pq,[:q ,@!=c1??1Bc^`nq gd1f-vM١N+b3pZ r@8.F1˅\w^tŵ\ε'J;^s˕\dl;;Ȏ{g1cr_1-q_#͜lw挑93-Μ12g4sFr#sF3g-Μ12g4sFr#sF3gܟfǸryg_Y@m\lrԌ ͨq155QbԾY3q]&Eh~_EΒQ̸8F\^ڍ{ꠁCk+̵1Y2x|=CKtGzhFߡWA.\zF@rV,C∿`> endobj 1587 0 obj << /Type /ObjStm /N 93 /First 888 /Length 3261 /Filter /FlateDecode >> stream xڭ[] }J"0$(Z4M} Inlaߗfx(QWso-,/a~ `rXbYi!b8Ji9uG';LMKxN͒'@-1HS M &81ɸH28Ts˗/pz/b֧{,Q뿟X>իvWdjyad Q uTa֞4T=驒W >||}{qO<4Ou|u.xL'=[!xp2L&S9Z> 0fWNT6i% qO$Vb4$]b8ZѐvDbh%FCb%C+1.q88!q%3]U_{R+9]C^֕7vl~;Yc=b@Un,(wM{7? -:}||zt_qAaax]Ugzasр~do*ǑBBҭEBPj T@~ sKh)DZIY# R^vt`  #vrAV-&? ,[/=8[v\{H2 f<|1S%И噣Q,ފ`ֿ>eԕJPI= Mj£m:zEӀxZr pԙ8Y'-gӉu ,apސCb$`=8D<L:vfzd(wS.p-:ti H&aAN_yדb=% F6M9>4>h2M\yr+dl tk Du҂&$@V ߦ3B ʂp퉞i֋'C=`KPU.7Pc';nfC*S*TM :*TjEG؋Ӊ̦',hU!K2Zrۖi9 Bꀛ졨PGXtYe:+crD{{QeHTRbGDev'{Znv@OuJmD)UnSpmrvS˳M.釣nHw^EQB]G==i?MID[ -b6Un~P#^Q*TdXVJBֳ\7YrͻG3o{ G%,1ηBP;C؆Pڌ(zPElr{+Ge豷zծP݁P]:;ٻ]X9*cE!u H Poc秇wpܶ.^˗ᵄe:̥ƺzʹΧ맸8e2NGno(yƒ fB4̍(0N0 ozXKsrgr PfrPF:#ѥfu\{RCz!?rC4㒁Tff< X =~+tLW 3Yfx#e? h3"4Hv@ 7:*z2r).r. [dɳqy.p gJ>!ͳ=Y=Y=XFPT|9]zCʠ".?GTwt7zšluH[tևQZI'8%)8Lz-∊8Q EueYc4: Y{~D93Mda]on7N;6!q09nD$6#* pdѡ`eY?Ԥwߤ`6=5nҋPg6ɀ4gsI|⺱} 4ݳ٤9OFS$|1s-/ԫIwP/&`6ɀ&ZK!MNJ?p#kt<U6-}nP mᔃ0I6o;,av; SؒVȒMVhsd˖VFDيBb7qfGMޡlLz27B,yZ8wdC$ARв74ٔ֌CIy+B)k?բ&&4`6ɱ4ھp:;n(LjbS4'[Zk 9ZBg&%ވ0]f~u$M mmi$jӶIc@e`dF@xn; Ԙn*/!wP)O^vF.V[$v,[ I)"̈́ on`~g06z TO6)y&%/#1[I̶@z-~oY.F[aU7l^;/G  v9^B}Y_n?ub}HR)Wt$n&b{!ީ>}eqݘ+Wƾ2v?{xwSiJ;Tڡ*^vGUC*PU N(7z?VwP8J T֝v5 A Xy'#QK)C&QajBPOTu:?4AP @\?^)_rr׼ON}<[}g5__\ow~w߼zëW"?2JY/q! 3Sa endstream endobj 1659 0 obj << /Type /XRef /Index [0 1660] /Size 1660 /W [1 3 1] /Root 1657 0 R /Info 1658 0 R /ID [<9DB964C1DAD93D8289548D6F322097DD> <9DB964C1DAD93D8289548D6F322097DD>] /Length 3961 /Filter /FlateDecode >> stream x%{lY=>sl-c'q&c;v|9'vv$$NAbvEETi vDꝶtێ`]tV Et7!hA\ R??|}<-/yyϼ+G՜Wȡ%̃*khGA:Z hḧ́5mBHXCk VϏ"ԅC{y@{VKXhh6pmHx FEӭ3h{hEG4us-:VE Σգi@7aa֌>4"GQ|@8΢2Za.C%vhۄ*e-1p &u06A8&FNhd4FM g,4vmp,͠=$ E{@x,QK`++h_YN~F] VѨ~JH|F]uʬ&vRv|B%tۧҊkٓnQM)fO(2R)fOf 'h}R̞EI{R̞C#o)K1{r!M1{ҎFS̞b $!fO1{¤^R̞bm=9F=I/5B̞bDEM=쉊*!fO1{ҏF=Im= GS̞\BLS̞ =q.b'쎹VN)ɘi 0t-1`v7vPٝr[PR0ٝY_)v;G-4& ;$,a57I0C&n&Ѧ fwsh7fwI)mA0I0LxGfwfw,߂k%"a[0J0.+2a': vncޱ7=z+=ёA(hEkN`A.s].WԝkJ4WDͮ]PAV9W1JT>`w]2Vxk} r`l- v.J5yZƳݬj2Z&< <jK@bP{x )lj T/ԅ5`HG]y YX'+]JCRH2Ƞ tUJeuJXוVs%`%fa.?0LV_ &IӀUZ~4J3>Sdv-Ti  Qi <4oY N%yۼ3CWD#LG$9"IHrDBʓ0Nۨ%{75 Ѽ}<^N@"rLGG{t%ͼ_\5G$>"N5JDA.GIm 7RkNޢ`м'a&]P&xܼ\穟&h9y@M}]2ke^Xl}e\jiMu^x4u+/D{`˼>Q5Jy!'_P3JV9XFr7543`D4oi@!^HJ7S$*a245>$gjuդ۬i 9LɠYI#o( )ɈٹHc@ 4W `lS[]`6 i$/HKHEb/>`w!8ɚ;5l)`%vZ욽tNVX{Sj`f?9iyP fy4k͞>5Z7}Ҵ/f/f-iVEo'h`]da]0mv. z̾XlY5̒_v ٫O-}sJaXGKٷZ4jUG5 N0-G,w-˽%i@ME˽E ,u-fT]Q&glXu{]h` /R U18g`/[<b@ 8h| ҎpU'&_WSӥ򣿧2и'M V qo\Ӏ:*R[~[e H%˯HYSLX_U~#__(:E[LY˿ܥrxݶw%~:+EW_PX G  ɱhbuu+|WqSgkU[ᕧU ݎ;udz<8ujo·b 4*µXOJrbpZNX/guV𔀣r+5@}Gm8[{@T@M+[xp;ꮘonUGFt7̯z>Wek&Dysͯ 5DuHS6o}i$ϑFc~뗥hсc y9GPeՏiQiEpԚG@7?H; A* -Gd^Ncǯh8h%z,'S ?`gwIݠIS7Ǒk@g]C`F \*134-_xN$7&mL)WnYp?L̀y0g/I=iu`oSK`C :x 6*=*@e_,Z@eT]V(uQkY 8(2++j((P9ՔgMF!e=Ⱥ{濬leV;QCՔՌUQ9YP E j#^0 (l P  6)@ dEv(l4F8%QMJF",2"rDmdFFmdFa~[yuhz8+>3]+~RG{V3滫::ڵnmyV_o+:YQj_ uTey]GlUSOMmfi&dߟ +l߇! Bj3d{6dO0d0T}8!;![!B4!Da7`/d'dg6d+6RHr}!r}!r}!!{}!;! mאgI~]]Ȏ\HK'd5d5gf]H˼>;;Y!h!U.zP_] )=v}]t}?ߴ|2o~ ' endstream endobj startxref 720652 %%EOF tsung-1.8.0/docs/_templates/0000755000201100017670000000000014377756736015463 5ustar nniclausdreamtsung-1.8.0/docs/_templates/README0000644000201100017670000000001714377756736016341 0ustar nniclausdreamHTML Templates tsung-1.8.0/docs/_static/0000755000201100017670000000000014377756736014754 5ustar nniclausdreamtsung-1.8.0/docs/_static/README0000644000201100017670000000003714377756736015634 0ustar nniclausdreamStatic files used by make html tsung-1.8.0/docs/conf.py0000644000201100017670000001706314377756736014634 0ustar nniclausdream# -*- coding: utf-8 -*- # # Tsung documentation build configuration file, created by # sphinx-quickstart on Thu Sep 19 12:07:49 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Tsung' copyright = u'2004-2023, Nicolas Niclausse' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.8.0' # The full version, including alpha/beta/rc tags. release = '1.8.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " 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 = {} # 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 = 'Tsungdoc' # -- 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', 'Tsung.tex', u'Tsung Documentation', u'Nicolas Niclausse', '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', 'tsung', u'Tsung Documentation', [u'Nicolas Niclausse'], 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', 'Tsung', u'Tsung Documentation', u'Nicolas Niclausse', 'Tsung', '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' tsung-1.8.0/docs/README0000644000201100017670000000273614377756736014216 0ustar nniclausdreamThese list of package are needed to generate the doc in pdf format under Debian Wheezy ii texlive 2012.20120611-5 all TeX Live: A decent selection of the TeX Live packages ii texlive-base 2012.20120611-5 all TeX Live: Essential programs and files ii texlive-binaries 2012.20120530-2+b1 i386 Binaries for TeX Live ii texlive-common 2012.20120611-1 all TeX Live: Base component ii texlive-doc-base 2012.20120611-1 all TeX Live: TeX Live documentation ii texlive-fonts-recommended 2012.20120611-5 all TeX Live: Recommended fonts ii texlive-lang-english 2012.20120611-2 all TeX Live: US and UK English ii texlive-lang-french 2012.20120611-2 all TeX Live: French ii texlive-latex-base 2012.20120611-5 all TeX Live: Basic LaTeX packages ii texlive-latex-extra 2012.20120611-2 all TeX Live: LaTeX supplementary packages ii texlive-latex-recommended 2012.20120611-5 all TeX Live: LaTeX recommended packages ii texlive-pictures 2012.20120611-5 all TeX Live: Graphics packages and programs tsung-1.8.0/docs/tsung-help.txt0000644000201100017670000000263214377756736016160 0ustar nniclausdream$ tsung -h Usage: tsung start|stop|debug|status Options: -f set configuration file (default is ~/.tsung/tsung.xml) (use - for standard input) -l set log directory where YYYYMMDD-HHMM dirs are created (default is ~/.tsung/log/) -i set controller id (default is empty) -r set remote connector (default is ssh) -s enable erlang smp on client nodes -p set maximum erlang processes per vm (default is 250000) -X

add additional erlang load paths (multiple -X arguments allowed) -m write monitoring output on this file (default is tsung.log) (use - for standard output) -F use long names (FQDN) for erlang nodes -L SSL session lifetime (600sec by default) -w warmup delay (default is 1 sec) -n disable web GUI (started by default on port 8091) -k keep web GUI (and controller) alive after the test has finished -v print version information and exit -6 use IPv6 for Tsung internal communications -x list of requests tag to be excluded from the run (separated by comma) -t erlang inet listening TCP port min (default: 64000) -T erlang inet listening TCP port max (default: 65500) -h display this help and exit tsung-1.8.0/docs/Makefile0000644000201100017670000001267014377756736014774 0ustar nniclausdream# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build 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) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .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)/* 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/Tsung.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tsung.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/Tsung" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tsung" @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." tsung-1.8.0/docs/reports.rst0000644000201100017670000002055514377756736015565 0ustar nniclausdream.. _statistics-reports: ====================== Statistics and Reports ====================== File format =========== By default, Tsung use its own format (see FAQ :ref:`what-format-stats`). .. index:: json **Since version 1.4.2**, you can configure Tsung to use a JSON format; however in this case, the tools :command:`tsung_stats.pl` and :command:`tsung_plotter` will not work with the JSON files. To enable JSON output, use:: Example output file with JSON:: { "stats": [ {"timestamp": 1317413841, "samples": []}, {"timestamp": 1317413851, "samples": [ {"name": "users", "value": 0, "max": 0}, {"name": "users_count", "value": 0, "total": 0}, {"name": "finish_users_count", "value": 0, "total": 0}]}, {"timestamp": 1317413861, "samples": [ {"name": "users", "value": 0, "max": 1}, {"name": "load", "hostname": "requiem", "value": 1, "mean": 0.0,"stddev": 0,"max": 0.0,"min": 0.0 ,"global_mean": 0 ,"global_count": 0}, {"name": "freemem", "hostname": "requiem", "value": 1, "mean": 2249.32421875,"stddev": 0,"max": 2249.32421875,"min": 2249.32421875 ,"global_mean": 0 ,"global_count": 0}, {"name": "cpu", "hostname": "requiem", "value": 1, "mean": 4.790419161676647,"stddev": 0,"max": 4.790419161676647,"min": 4.790419161676647 ,"global_mean": 0 ,"global_count": 0}, {"name": "session", "value": 1, "mean": 387.864990234375,"stddev": 0,"max": 387.864990234375,"min": 387.864990234375 ,"global_mean": 0 ,"global_count": 0}, {"name": "users_count", "value": 1, "total": 1}, {"name": "finish_users_count", "value": 1, "total": 1}, {"name": "request", "value": 5, "mean": 75.331787109375,"stddev": 46.689242405019954,"max": 168.708984375,"min": 51.744873046875 ,"global_mean": 0 ,"global_count": 0}, {"name": "page", "value": 1, "mean": 380.7548828125,"stddev": 0.0,"max": 380.7548828125,"min": 380.7548828125 ,"global_mean": 0 ,"global_count": 0}, {"name": "connect", "value": 1, "mean": 116.70703125,"stddev": 0.0,"max": 116.70703125,"min": 116.70703125 ,"global_mean": 0 ,"global_count": 0}, {"name": "size_rcv", "value": 703, "total": 703}, {"name": "size_sent", "value": 1083, "total": 1083}, {"name": "connected", "value": 0, "max": 0}, {"name": "http_304", "value": 5, "total": 5}]}]} Available stats =============== .. index:: page * ``request`` Response time for each request. * ``page`` Response time for each set of requests (a page is a group of request not separated by a thinktime). * ``connect`` Duration of the connection establishment. * ``reconnect`` Number of reconnection. * ``size_rcv`` Size of responses in bytes. * ``size_sent`` Size of requests in bytes. * ``session`` Duration of a user's session. * ``users`` Number of simultaneous users (it's session has started, but not yet finished). * ``connected`` number of users with an opened TCP/UDP connection (example: for HTTP, during a think time, the TCP connection can be closed by the server, and it won't be reopened until the thinktime has expired). **new in 1.2.2**. * custom transactions The mean response time (for requests, page, etc.) is computed every 10 sec (and reset). That's why you have the highest mean and lowest mean values in the Stats report. **Since version 1.3.0**, the mean for the whole test is also computed. HTTP specific stats: -------------------- * counter for each response status (200, 404, etc.) Jabber specific stats: ---------------------- * ``request_noack`` Counter of ``no_ack`` requests. Since response time is meaningless with ``no_ack`` requests, we keep a separate stats for this. **new in 1.2.2**. * ``async_unknown_data_rcv`` Only if bidi is true for a session. Count the number of messages received from the server without doing anything. **new in 1.2.2**. * ``async_data_sent`` Only if bidi is true for a session. Count the number of messages sent to the server in response of a message received from the server. **new in 1.2.2**. OS monitoring stats: -------------------- * ``{load,}`` System load average during the last minute * ``{cpu,}`` Free Memory Design ====== A bit of explanation on the design and internals of the statistics engine: Tsung was designed to handle thousands of requests/sec, for very long period of times (several hours) so it do not write all data to the disk (for performance reasons). Instead it computes on the fly an estimation of the mean and standard variation for each type of data, and writes these estimations every 10 seconds to the disk (and then starts a new estimation for the next 10 sec). These computations are done for two kinds of data: .. index:: sample .. index:: sample_counter * ``sample``, for things like response time * ``sample_counter`` when the input is a cumulative one (number of packet sent for ex.). There are also two other types of useful data (no averaging is done for those): * ``counter``: a simple counter, for HTTP status code for ex. * ``sum`` for ex. the cumulative HTTP response's size (it gives an estimated bandwidth usage). Generating the report ===================== **Since version 1.6.0**, you can use the embedded web server started by the controller on port 8091. So for example if your controller is running on ``node0``, use the URL http://node0:8091/ in your browser. It will display the current status of Tsung (see :ref:`fig-dashboard` ) and generate on the fly the report and graphs. There's also an option when you start Tsung to keep the controller alive, even when the test if finished, in order to use the embedded web server (see ``-k`` option). By default the web server will stop when the test is finished. .. _fig-dashboard: .. figure:: ./images/tsung-dashboard.png Dashboard You can still generate the reports by manually during or after the tests: cd to the log directory of your test (say :file:`~/.tsung/log/20040325-16:33/`) and use the script :command:`tsung_stats.pl`:: /usr/lib/tsung/bin/tsung_stats.pl .. note:: You can generate the statistics even when the test is running! use **--help** to view all available options:: Available options: [--help] (this help text) [--verbose] (print all messages) [--debug] (print receive without send messages) [--dygraph] use dygraphs (http://dygraphs.com) to render graphs [--noplot] (don't make graphics) [--gnuplot ] (path to the gnuplot binary) [--nohtml] (don't create HTML reports) [--logy] (logarithmic scale for Y axis) [--tdir ] (Path to the HTML tsung templates) [--noextra (don't generate graphics from extra data (os monitor, etc) [--rotate-xtics (rotate legend of x axes) [--stats ] (stats file to analyse, default=tsung.log) [--img_format ] (output format for images, default=png available format: ps, svg, png, pdf) Version **1.4.0** adds a new graphical output based on http://dygraphs.com. Tsung summary ============= Figure :ref:`fig-report` shows an example of a summary report. .. _fig-report: .. figure:: ./images/tsung-report.png Report Graphical overview ================== Figure :ref:`fig-graph` shows an example of a graphical report. .. _fig-graph: .. figure:: ./images/tsung-graph.png Graphical output Tsung Plotter ============= Tsung-Plotter (:command:`tsplot`} command) is an optional tool recently added in the Tsung distribution (it is written in Python), useful to compare different tests ran by Tsung. :command:`tsplot` is able to plot data from several :file:`tsung.log` files onto the same charts, for further comparisons and analyzes. You can easily customize the plots you want to generate by editing simple configuration files. You can get more information in the manual page of the tool (:command:`man tsplot`). Example of use:: tsplot "First test" firsttest/tsung.log "Second test" secondtest/tsung.log -d outputdir Here's an example of the charts generated by tsplot (figure :ref:`fig-graph-tsplot`): .. _fig-graph-tsplot: .. figure:: ./images/connected.png Graphical output of ``tsplot`` RRD === A contributed perl script :command:`tsung-rrd.pl` is able to create rrd files from the Tsung log files. It's available in :file:`/usr/lib/tsung/bin/tsung-rrd.pl`. tsung-1.8.0/docs/references.rst0000644000201100017670000000136514377756736016206 0ustar nniclausdream========== References ========== * Tsung home page: http://tsung.erlang-projects.org/ * Tsung description (French) [#1]_ * Erlang web site http://www.erlang.org/ * Erlang programmation, Mickaël Rémond, Editions Eyrolles, 2003 [#2]_ * **Making reliable system in presence of software errors**, Doctoral Thesis, Joe Armstrong, Stockholm, 2003 [#3]_ * **Tutorial on How to write a Tsung plugin**, written by t ty, http://www.process-one.net/en/wiki/Writing_a_Tsung_plugin/ .. [#1] http://www.erlang-projects.org/Members/mremond/events/dossier_de_presentat/block_10766817551485/file .. [#2] http://www.editions-eyrolles.com/php.accueil/Ouvrages/ouvrage.php3?ouv_ean13=9782212110791 .. [#3] http://www.sics.se/~joe/thesis/armstrong_thesis_2003.pdf tsung-1.8.0/docs/proxy.rst0000644000201100017670000000350214377756736015241 0ustar nniclausdream .. index:: proxy .. index:: tsung-recorder .. _tsung-recorder: ======================== Using the proxy recorder ======================== The recorder has three plugins: for HTTP, WebDAV and for PostgreSQL. To start it, run :command:`tsung-recorder -p start`, where **PLUGIN** can be *http*, *webdav* or *pgsql* for PostgreSQL. The default plugin is **http**. The proxy is listening to port **8090**. You can change the port with :option:`-L portnumber`. To stop it, use :command:`tsung-recorder stop`. The recorded session is created as :file:`~/.tsung/tsung_recorderYYYMMDD-HH:MM.xml`; if it doesn't work, take a look at :file:`~/.tsung/log/tsung.log-tsung_recorder@hostname` .. index:: record_tag During the recording, you can add custom tag in the XML file, this can be useful to set transactions or comments: :command:`tsung-recorder record_tag "''` Once a session has been created, you can insert it in your main configuration file, either by editing by hand the file, or by using an ENTITY declaration, like: .. code-block:: xml ]> ... &mysession1; PostgreSQL ========== For PostgreSQL, the proxy will connect to the server at IP 127.0.0.1 and port 5432. Use **-I serverIP** to change the IP and **-P portnumber** to change the port. HTTP and WEBDAV =============== For HTTPS recording, use **http://-** instead of **https://** in your browser **New in 1.2.2**: For HTTP, you can configure the recorder to use a parent proxy (but this will not work for https). Add the :option:`-u` option to enable parent proxy, and use **-I serverIP** to set the IP and **-P portnumber** to set the port of the parent. tsung-1.8.0/docs/introduction.rst0000644000201100017670000000736614377756736016615 0ustar nniclausdream============ Introduction ============ What is Tsung? =============== Tsung (formerly IDX-Tsunami) is a distributed load testing tool. It is protocol-independent and can currently be used to stress HTTP, WebDAV, SOAP, PostgreSQL, MySQL, AMQP, MQTT, LDAP and Jabber/XMPP servers. It is distributed under the GNU General Public License version 2. What is Erlang and why is it important for Tsung? ================================================== Tsung's main strength is its ability to simulate a huge number of simultaneous user from a single machine; moreover, you can distribute the users on cluster for machines. When used on cluster, you can generate a really impressive load on a server with a modest cluster, easy to set-up and to maintain. You can also use Tsung on a cloud like EC2. Tsung is developed in Erlang and this is where the power of Tsung resides. Erlang is a *concurrency-oriented* programming language. Tsung is based on the Erlang OTP (Open Telecom Platform) and inherits several characteristics from Erlang: Performance Erlang has been made to support hundred thousands of lightweight processes in a single virtual machine. Scalability Erlang runtime environment is naturally distributed, promoting the idea of process's location transparency. Fault-tolerance Erlang has been built to develop robust, fault-tolerant systems. As such, wrong answer sent from the server to Tsung does not make the whole running benchmark crash. More information on Erlang on http://www.erlang.org. Tsung background ================ History: * Tsung development was started by Nicolas Niclausse in 2001 as a distributed jabber load stress tool for internal use at http://IDEALX.com/ (now OpenTrust). It has evolved as an open-source multi-protocol load testing tool several months later. The HTTP support was added in 2003, and this tool has been used for several industrial projects. It is now hosted on github, and several companies provide profesionnal support. The list of contributors is available in the source archive at https://github.com/processone/tsung/blob/master/CONTRIBUTORS. * It is an industrial strength implementation of a *stochastic model* for real users simulation. User events distribution is based on a Poisson Process. More information on this topic in: Z. Liu, N. Niclausse, and C. Jalpa-Villanueva. **Traffic Model and Performance Evaluation of Web Servers**. *Performance Evaluation, Volume 46, Issue 2-3, October 2001*. * This model has already been tested in the INRIA *WAGON* research prototype (Web trAffic GeneratOr and beNchmark). WAGON was used in the http://www.vthd.org/ project (Very High Broadband IP/WDM test platform for new generation Internet applications, 2000-2004). Tsung has been used for very high load tests: * *Jabber/XMPP* protocol: * 90,000 simultaneous Jabber users on a 4-node Tsung cluster (3xSun V240 + 1 Sun V440). * 10,000 simultaneous users. Tsung was running on a 3-computers cluster (CPU 800MHz). * 2,000,000 concurrent users on a single m4.10xlarge instance on EC2 to tests ejabberd scalability * *HTTP and HTTPS* protocol: * 12,000 simultaneous users. Tsung were running on a 4-computers cluster (in 2003). The tested platform reached 3,000 https requests per second. * 10 million simultaneous users running on a 75-computers cluster, generating more than one million requests per second. Tsung has been used at: * *DGI* (Direction Générale des impôts): French finance ministry * *Cap Gemini Ernst & Young* * *IFP* (Institut Français du Pétrole): French Research Organization for Petroleum * *LibertySurf* * *Sun* (TM) for their Moodlerooms platform on Niagara processors: https://blogs.oracle.com/kevinr/resource/Moodle-Sun-RA.pdf * and many other companies tsung-1.8.0/docs/installation.rst0000644000201100017670000000645314377756736016571 0ustar nniclausdream============ Installation ============ This package has been tested on Linux, FreeBSD and Solaris. A port is available on Mac OS X. It should work on Erlang supported platforms (Linux, Solaris, \*BSD, Win32 and Mac OS X). On Mac OS X you can install Tsung via Homebrew (http://brew.sh/): :command:`brew install tsung`. Dependencies ============ * **Erlang/OTP R16B03** and up (`download `_). * **pgsql module** made by Christian Sunesson (for the PostgreSQL plugin): sources available at http://jungerl.sourceforge.net/ . The module is included in the source and binary distribution of Tsung. It is released under the EPL License. * **mysql module** made by Magnus Ahltorp & Fredrik Thulin (for the mysql plugin): sources available at http://www.stacken.kth.se/projekt/yxa/. The modified module is included in the source and binary distribution of Tsung. It is released under the three-clause BSD License. * **mochiweb** libs (for XPath parsing, optionally used for dynamic variables in the HTTP plugin): sources available at https://github.com/mochi/mochiweb. The module is included in the source and binary distribution of Tsung. It is released under the MIT License. * **gnuplot** and **perl5** (optional; for graphical output with ``tsung_stats.pl`` script). The Template Toolkit is used for HTML reports (see http://template-toolkit.org/). * **python** and **matplotlib** (optional; for graphical output with ``tsung-plotter``). * for distributed tests, you need SSH access to remote machines without password (use a RSA/DSA key without passphrase or ssh-agent). Alternatively rsh is also supported. * bash Compilation =========== To compile Tsung, just download the latest version from http://tsung.erlang-projects.org/dist/ and run:: ./configure make make install If you want to download the latest development version, use git: :command:`git clone https://github.com/processone/tsung.git`, see also https://github.com/processone/tsung. You can also build packages with :command:`make deb` (on Debian and Ubuntu) and :command:`make rpm` (on Fedora, RHEL and other rpm based distribution). Configuration ============= The default configuration file is :file:`~/.tsung/tsung.xml` (there are several sample files in :file:`/usr/share/doc/tsung/examples`). Log files are saved in :file:`~/.tsung/log/`. A new subdirectory is created for each test using the current date and time as name, e.g. :file:`~/.tsung/log/20040217-0940`. Running ======= Two commands are installed in the directory :file:`$PREFIX/bin`: ``tsung`` and ``tsung-recorder``. A man page is available for both commands. .. literalinclude:: tsung-help.txt A typical way of using tsung is to run: :command:`tsung -f myconfigfile.xml start`. The command will print the current log directory created for the test, and wait until the test is over. By default an embedded web server will be started on the controller node and will listen on the 8091 port (this can be disabled with the `-n` option. Feedback ======== Use the `Tsung mailing list `_ if you have suggestions or questions about Tsung. You can also use the bug tracker available at https://github.com/processone/tsung/issues You can also try the #tsung IRC channel on Freenode. tsung-1.8.0/docs/index.rst0000644000201100017670000000176114377756736015174 0ustar nniclausdream.. Tsung documentation master file, created by sphinx-quickstart on Thu Sep 19 12:07:49 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Tsung's documentation! ================================= .. rubric:: Everything you need to know about Tsung .. sidebar:: About Tsung Tsung is a high-performance benchmark framework for various protocols including HTTP, XMPP, LDAP, etc * **Website**: `tsung.erlang-projects.org `_ * **Source code**: `github.com/processone/tsung `_ * **Bugtracker**: ` ` .. toctree:: :maxdepth: 2 :numbered: introduction features installation benchmark proxy configuration reports references acknowledgment faq errorslist changelog dtd Indices and tables ================== * :ref:`genindex` * :ref:`search` tsung-1.8.0/docs/features.rst0000644000201100017670000001505714377756736015706 0ustar nniclausdream.. _ab: http://httpd.apache.org/docs/current/programs/ab.html ======== Features ======== Tsung main features =================== * *High Performance*: ``Tsung`` can simulate a huge number of simultaneous users per physical computer: It can simulates thousands of users on a single CPU (Note: a simulated user is not always active: it can be idle during a ``thinktime`` period). Traditional injection tools can hardly go further than a few hundreds (Hint: if all you want to do is requesting a single URL in a loop, use ab_; but if you want to build complex scenarios with extended reports, ``Tsung`` is for you). * *Distributed*: the load can be distributed on a cluster of client machines * *Multi-Protocols* using a plug-in system: HTTP (both standard web traffic and SOAP), WebDAV, Jabber/XMPP and PostgreSQL are currently supported. LDAP and MySQL plugins were first included in the 1.3.0 release. * *SSL* support * *Several IP addresses* can be used on a single machine using the underlying OS IP Aliasing * *OS monitoring* (CPU, memory and network traffic) using Erlang agents on remote servers or *SNMP* * *XML configuration system*: complex user's scenarios are written in XML. Scenarios can be written with a simple browser using the Tsung recorder (HTTP and PostgreSQL only). * *Dynamic scenarios*: You can get dynamic data from the server under load (without writing any code) and re-inject it in subsequent requests. You can also loop, restart or stop a session when a string (or regexp) matches the server response. * *Mixed behaviours*: several :ref:`sessions ` can be used to simulate different type of users during the same benchmark. You can define the proportion of the various behaviours in the benchmark scenario. * *Stochastic processes*: in order to generate a realistic traffic, user thinktimes and the arrival rate can be randomized using a probability distribution (currently exponential) HTTP related features ===================== * HTTP/1.0 and HTTP/1.1 support * GET, POST, PUT, DELETE, HEAD, OPTIONS, PURGE and PATCH requests * Cookies: Automatic cookies management (but you can also manually add more cookies) * 'GET If-modified since' type of request * WWW-authentication Basic and Digest. OAuth 1.0 * User Agent support * Any HTTP Headers can be added * Proxy mode to record sessions using a Web browser * SOAP support using the HTTP mode (the SOAPAction HTTP header is handled). * HTTP server or proxy server load testing. WEBDAV related features ======================= The WebDAV (:RFC:`4918`) plugin is a superset of the HTTP plugin. It adds the following features (some versioning extensions to WebDAV (:RFC:`3253`) are also supported): * Methods implemented: DELETE, CONNECT, PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, MKCOL, REPORT, OPTIONS, MKACTIVITY, CHECKOUT, MERGE * Recording of DEPTH, IF, TIMEOUT OVERWRITE, DESTINATION, URL and LOCK-TOKEN Headers. Jabber/XMPP related features ============================ * Authentication (plain-text, digest and sip-digest). STARTTLS * presence and register messages * Chat messages to online or offline users * MUC: join room, send message in room, change nickname * Roster set and get requests * Global users' synchronization can be set on specific actions * BOSH & XMPP over Websocket * raw XML messages * PubSub * Multiple vhost instances supported * privacy lists: get all privacy list names, set list as active PostgreSQL related features =========================== * Basic and MD5 Authentication * Simple Protocol * Extended Protocol (new in version **1.4.0** ) * Proxy mode to record sessions MySQL related features ====================== This plugin is experimental. It works only with MySQL version 4.1 and higher. * Secured Authentication method only (MySQL >= 4.1) * Basic Queries Websocket related features ========================== This plugin is experimental. It only supports :RFC:`6455` currently. For used as a server type, it works like other transport protocol like tcp and udp, any application specific protocol data can be send on it. You can find examples used as session type in examples/websocket.xml. * Both as a server type and session type AMQP related features ===================== This plugin is experimental. It only supports AMQP-0.9.1 currently. You can find examples in examples/amqp.xml. * Basic publish and consume * Publisher confirm and consumer ack * QoS MQTT related features ===================== This plugin is experimental. It supports MQTT V3.1. You can find examples in examples/mqtt.xml. * Connect to mqtt broker with options * Publish mqtt messages to the broker * Subscribe/unsubscribe topics * Support QoS 0 and QoS 1 LDAP related features ===================== * Bind * Add, modify and search queries * Starttls Raw plugin related features =========================== * TCP / UDP / SSL compatible * raw messages * no_ack, local or global ack for messages Complete reports set ==================== Measures and statistics produced by Tsung are extremely feature-full. They are all represented as a graphic. ``Tsung`` produces statistics regarding: * *Performance*: response time, connection time, decomposition of the user scenario based on request grouping instruction (called *transactions*), requests per second * *Errors*: Statistics on page return code to trace errors * *Target server behaviour*: An Erlang agent can gather information from the target server(s). Tsung produces graphs for CPU and memory consumption and network traffic. SNMP and munin is also supported to monitor remote servers. \par Note that ``Tsung`` takes care of the synchronization process by itself. Gathered statistics are «synchronized». It is possible to generate graphs during the benchmark as statistics are gathered in real-time. Highlights ========== ``Tsung`` has several advantages over other injection tools: * *High performance* and *distributed benchmark*: You can use Tsung to simulate tens of thousands of virtual users. * *Ease of use*: The hard work is already done for all supported protocol. No need to write complex scripts. Dynamic scenarios only requires small trivial piece of code. * *Multi-protocol support*: ``Tsung`` is for example one of the only tool to benchmark SOAP applications * *Monitoring* of the target server(s) to analyze the behaviour and find bottlenecks. For example, it has been used to analyze cluster symmetry (is the load properly balanced ?) and to determine the best combination of machines on the three cluster tiers (Web engine, EJB engine and database) tsung-1.8.0/docs/faq.rst0000644000201100017670000002755514377756736014645 0ustar nniclausdream.. index:: faq .. _faq: .. _ab: http://httpd.apache.org/docs/current/programs/ab.html ========================== Frequently Asked Questions ========================== Can't start distributed clients: timeout error ============================================== Most of the time, when a crash happened at startup without any traffic generated, the problem arise because the main Erlang controller node cannot create a "slave" Erlang virtual machine. The message looks like:: Can't start newbeam on host 'XXXXX (reason: timeout) ! Aborting! The problem is that the Erlang slave module cannot start a remote slave node. You can test this using this simple command on the controller node (remotehost is the name of the client node):: >erl -rsh ssh -sname foo -setcookie mycookie Eshell V5.4.3 (abort with ^G) (foo@myhostname)1>slave:start(remotehost,bar,"-setcookie mycookie"). You should see this:: {ok,bar@remotehost} If you got ``{error,timeout}``, it can be caused by several problems: * ssh in not working (you must have a key without passphrase, or use an agent) * Tsung and Erlang are not installed on all clients nodes * Erlang version or location (install path) is not the same on all clients nodes * A firewall is dropping Erlang packets: Erlang virtual machines use several TCP ports (dynamically generated) to communicate (if you are using EC2, you may have to change the Security Group that is applied on the VMs used for Tsung: open port range 0 - 65535) * SELinux: You should disable SELinux on all clients. * Bad :file:`/etc/hosts`: This one is wrong (real hostname should not refer to localhost/loopback):: 127.0.0.1 localhost myhostname This one is good:: 127.0.0.1 localhost 192.168.3.2 myhostname * sshd configuration: For example, for SuSE 9.2 sshd is compiled with restricted set of paths (ie. when you shell into the account you get the users shell, when you execute a command via ssh you don't) and this makes it impossible to start an Erlang node (if Erlang is installed in :file:`/usr/local` for example). Run:: ssh myhostname erl If the Erlang shell doesn't start then check what paths sshd was compiled with (in SuSE see :file:`/etc/ssh/sshd_config`) and symlink from one of the approved paths to the Erlang executable (thanks to Gordon Guthrie for reporting this). * old beam processes (Erlang virtual machines) running on client nodes: kill all beam processes before starting Tsung. Note that you do not need to use the ``127.0.0.1`` address in the configuration file. It will not work if you use it as the injection interface. The shortname of your client machine should not refer to this address. **Warning** Tsung launches a new Erlang virtual machine to do the actual injection even when you have only one machine in the injection cluster (unless ``use_controller_vm`` is set to true). This is because it needs to by-pass some limit with the number of open socket from a single process (1024 most of the time). The idea is to have several system processes (Erl beam) that can handle only a small part of the network connection from the given computer. When the ``maxusers`` limit (simultaneous) is reach, a new Erlang beam is launched and the newest connection can be handled by the new beam). **New in 1.1.0**: If you don't use the distributed feature of Tsung and have trouble to start a remote beam on a local machine, you can set the ``use_controller_vm`` attribute to true:: Tsung crashes when I start it ============================= Does your Erlang system has SSL support enabled ? to test it:: > erl Eshell V5.2 (abort with ^G) 1> ssl:start(). you should see 'ok' .. _faq-emfile-label: Why do i have error_connect_emfile errors? ========================================== :index:`emfile` error means : **too many open files** This happens usually when you set a high value for :ref:`maxusers-label` (in the ```` section) (the default value is 800). The errors means that you are running out of file descriptors; you must check that :ref:`maxusers-label` is less than the maximum number of file descriptors per process in your system (see :command:`ulimit -n`). You can either raise the limit of your operating system (see :file:`/etc/security/limits.conf` for Linux) or decrease :ref:`maxusers-label` Tsung will have to start several virtual machine on the same host to bypass the maxusers limit. It could be good if you want to test a large number of users to make some modifications to your system before launching Tsung: * Put the domain name into :file:`/etc/hosts` if you don't want the DNS overhead and you only want to test the target server * Increase the maximum number of open files and customize TCP settings in :file:`/etc/sysctl.conf`. For example:: net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.ip_local_port_range = 1024 65000 fs.file-max = 65000 Tsung still crashes/fails when I start it! ========================================== First look at the log file :file:`~/.tsung/log/XXX/tsung_controller@yourhostname` to see if there is a problem. If the file is not created and a crashed dump file is present, maybe you are using a binary installation of Tsung not compatible with the version of Erlang you used. If you see nothing wrong, you can compile Tsung with full debugging: recompile with :command:`make debug`, and don't forget to set the loglevel to ``debug`` in the XML file (see :ref:`tsung.xml log levels `). To start the debugger or see what happen, start Tsung with the ``debug`` argument instead of ``start``. You will have an Erlang shell on the ``tsung_controller`` node. Use :command:`toolbar:start().` to launch the graphical tools provided by Erlang. Can I dynamically follow redirect with HTTP? ============================================ If your HTTP server sends 30X responses (:index:`redirect`) with dynamic URLs, you can handle this situation using a dynamic variable: .. code-block:: xml You can even handle the case where the server use several redirections successively using a repeat loop (this works only with version 1.3.0 and up): .. code-block:: xml .. _what-format-stats: What is the format of the stats file tsung.log? =============================================== Sample tsung.log:: # stats: dump at 1218093520 stats: users 247 247 stats: connected 184 247 stats: users_count 184 247 stats: page 187 98.324 579.441 5465.940 2.177 9.237 595 58 stats: request 1869 0.371 0.422 5.20703125 0.115 0.431 7444062 581 stats: connect 186 0.427 0.184 4.47216796875 0.174 0.894 88665254 59 stats: tr_login 187 100.848 579.742 5470.223 2.231 56.970 91567888 58 stats: size_rcv 2715777 3568647 stats: 200 1869 2450 stats: size_sent 264167 347870 # stats: dump at 1218093530 stats: users 356 356 stats: users_count 109 356 stats: connected -32 215 stats: page 110 3.346 0.408 5465.940 2.177 77.234 724492 245 stats: request 1100 0.305 0.284 5.207 0.115 0.385 26785716 2450 stats: connect 110 0.320 0.065 4.472 0.174 0.540 39158164 245 stats: tr_login 110 3.419 0.414 5470.223 2.231 90.461 548628831 245 stats: size_rcv 1602039 5170686 stats: 200 1100 3550 stats: size_sent 150660 498530 ... the format is, for ``request``, ``page``, ``session`` and transactions ``tr_XXX``:: stats: name, 10sec_count, 10sec_mean, 10sec_stddev, max, min, mean, count or for HTTP returns codes, ``size_sent`` and ``size_rcv``:: stats: name, count(during the last 10sec), totalcount(since the beginning) How can I compute percentile/quartiles/median for transactions or requests response time? ========================================================================================= It's not directly possible. But since **version 1.3.0**, you can use a new experimental statistic backend: set ``backend="fullstats"`` in the ```` section of your configuration file (also see :ref:`sec-file-structure-label`). This will print every statistics data in a raw format in a file named :file:`tsung-fullstats.log`. **Warning**: this may impact the performance of the controller node (a lot of data has to be written to disk). The data looks like:: {sum,connected,1} {sum,connected,-1} [{sample,request,214.635}, {sum,size_rcv,268}, {sample,page,831.189}, {count,200}, {sum,size_sent,182}, {sample,connect,184.787}, {sample,request,220.974}, {sum,size_rcv,785}, {count,200}, {sum,size_sent,164}, {sample,connect,185.482}] {sum,connected,1} [{count,200},{sum,size_sent,161},{sample,connect,180.812}] [{sum,size_rcv,524288},{sum,size_rcv,524288}] Since version **1.5.0**, a script :command:`tsung_percentile.pl` is provided to compute the percentiles from this file. How can I specify the number of concurrent users? ================================================= You can't. But it's on purpose: the load generated by Tsung is dependent on the arrival time between new clients. Indeed, once a client has finished his session in Tsung, it stops. So the number of concurrent users is a function of the arrival rate and the mean session duration. For example, if your web site has 1,000 visits/hour, the arrival rate is ``1000/3600 = 0.2778`` visits/second. If you want to simulate the same load, set the inter-arrival time is to ``1/0.27778 = 3.6 sec`` (e.g. ```` in the ``arrivalphase`` node in the XML config file). .. _sec-faq-snmp-label: SNMP monitoring doesn't work?! ============================== It use SNMP v1 and the "public" community. It has been tested with http://net-snmp.sourceforge.net/. You can try with :command:`snmpwalk` to see if your snmpd config is ok:: >snmpwalk -v 1 -c public IP-OF-YOUR-SERVER .1.3.6.1.4.1.2021.4.5.0 UCD-SNMP-MIB::memTotalReal.0 = INTEGER: 1033436 SNMP doesn't work with Erlang R10B and Tsung older than 1.2.0. There is a small bug in the ``snmp_mgr`` module in old Erlang release (R9C-0). This is fixed in Erlang R9C-1 and up, but you can apply this patch to make it work on earlier version:: --- lib/snmp-3.4/src/snmp_mgr.erl.orig 2004-03-22 15:21:59.000000000 +0100 +++ lib/snmp-3.4/src/snmp_mgr.erl 2004-03-22 15:23:46.000000000 +0100 @@ -296,6 +296,10 @@ end; is_options_ok([{recbuf,Sz}|Opts]) when 0 < Sz, Sz =< 65535 -> is_options_ok(Opts); +is_options_ok([{receive_type, msg}|Opts]) -> + is_options_ok(Opts); +is_options_ok([{receive_type, pdu}|Opts]) -> + is_options_ok(Opts); is_options_ok([InvOpt|_]) -> {error,{invalid_option,InvOpt}}; is_options_ok([]) -> true. How can i simulate a fix number of users? ========================================= Use ``maxnumber`` to set the max number of concurrent users in a phase, and if you want Tsung to behave like ab_, you can use a loop in a session (to send requests as fast as possible); you can also define a max ``duration`` in ````. .. code-block:: xml tsung-1.8.0/docs/errorslist.rst0000644000201100017670000000405614377756736016275 0ustar nniclausdream=========== Errors list =========== error_closed ------------ Only for non persistent session (XMPP); the server unexpectedly closed the connection; the session is aborted. error_inet_ ---------------------- Network error; see http://www.erlang.org/doc/man/inet.html for the list of all errors. error_unknown_data ------------------ Data received from the server during a thinktime (not for unparsed protocol like XMPP). The session is aborted. error_unknown_msg ----------------- Unknown message received (see the log files for more information). The session is aborted. error_unknown ------------- Abnormal termination of a session, see log file for more information. error_repeat_ ------------------------- Error in a repeat loop (undefined dynamic variable usually). error_send_ ---------------------- Error while sending data to the server, see http://www.erlang.org/doc/man/inet.html for the list of all errors. error_send ---------- Unexpected error while sending data to the server, see the logfiles for more information. error_connect_ ------------------------- Error while establishing a connection to the server. See http://www.erlang.org/doc/man/inet.html for the list of all errors. error_no_online --------------- XMPP: No online user available (usually for a chat message destinated to a online user) error_no_offline ---------------- XMPP: No offline user available (usually for a chat message destinated to a offline user) error_no_free_userid -------------------- For XMPP: all users Id are already used (``userid_max`` is too low ?) error_next_session ------------------ A clients fails to gets its session parameter from the config_server; the controller may be overloaded ? error_mysql_ ------------------- Error reported by the mysql server (see http://dev.mysql.com/doc/refman/5.0/en/error-messages-server.html) error_mysql_badpacket --------------------- Bad packet received for mysql server while parsing data. error_pgsql ----------- Error reported by the postgresql server. tsung-1.8.0/docs/dtd.rst0000644000201100017670000000012214377756736014626 0ustar nniclausdream.. index:: dtd tsung-1.0.dtd ============= .. literalinclude:: ../tsung-1.0.dtd tsung-1.8.0/docs/conf-sessions.rst0000644000201100017670000011603614377756736016660 0ustar nniclausdream.. index:: session .. _sessions-label: ======== Sessions ======== Sessions define the content of the scenario itself. They describe the requests to execute. Each session has a given probability. This is used to decide which session a new user will execute. The sum of all session's probabilities must be 100. **Since Tsung 1.5.0**, you can use weights instead of probabilities. In the following example, there will be twice as many sessions of type s1 than s2. .. code-block:: xml A transaction is just a way to have customized statistics. Say if you want to know the response time of the login page of your website, you just have to put all the requests of this page (HTML + embedded pictures) within a transaction. In the example below, the transaction called ``index_request`` will gives you in the statistics/reports the mean response time to get ``index.en.html + header.gif``. Be warn that If you have a thinktime inside the transaction, the thinktime will be part of the response time. .. index:: thinktimes Thinktimes ^^^^^^^^^^ You can set static or random thinktimes to separate requests. By default, a random thinktime will be a exponential distribution with mean equals to ``value``. .. code-block:: xml In this case, the thinktime will be an exponential distribution with a mean equals to 20 seconds. **Since version 1.3.0**, you can also use a range ``[min:max]`` instead of a mean for random thinktimes (the distribution will be uniform in the interval): .. code-block:: xml **Since version 1.4.0**, you can use a dynamic variable to set the thinktime value: .. code-block:: xml You can also synchronize all users using the ``wait_global`` value: .. code-block:: xml which means: wait for all (N) users to be connected and waiting for the global lock (the value can be set using the option ``
tsung-1.8.0/src/templates/graph.thtml0000644000201100017670000001175014377756736017342 0ustar nniclausdream[% INCLUDE header.thtml %]

Response Time

Transactions and PagesRequests and connection establishment
transaction response time

Response time in msec for pages and transactions (a page is a group of requests not separated by a thinktime).

Info »
mean request response time
connect
Mean duration (in msec) of the connection establishment only.
request
Mean duration of requests (in msec).
Info »

Throughput

[% IF async %] [% IF bosh %][% END %] [% IF bosh %] [% END %] [% END %]
TransactionsRequests
transaction rate req/sec
Noack/BidiBOSH
req/sec req/sec
Network trafficNew Users
Kb/sec visit/sec

Simultaneous Users

[% IF match %] [% END %] [% IF match %] [% END %]
Simultaneous UsersMatching responses
Users
users
Number of simultaneous users (it's session has started, but not yet finished).
connected
number of users with an opened TCP/UDP connection (example: for HTTP, during a think time, the TCP connection can be closed by the server, and it won't be reopened until the thinktime has expired)
Info »
Match
[% IF os_mon %]

Server OS monitoring

[% IF os_mon_other %] [% USE table(os_mon_other, cols=2) %] [% FOREACH row = table.rows %] [% FOREACH key = row %] [% END %] [% FOREACH key = row %] [% END %] [% END %] [% END %]
CPU%Free Memory
cpu free memory
CPU Load
load
$key
[% IF key %] other [% END %]
[% END %] [% IF http %]

HTTP return code Status (rate)

HTTP_CODE-rate
[% END %] [% IF errors %]

Errors (rate)

Errors-rate
[% END %] [% INCLUDE footer.thtml %] tsung-1.8.0/src/templates/graph_dy.thtml0000644000201100017670000001351514377756736020037 0ustar nniclausdream[% INCLUDE header.thtml %]

Response Time

TransactionsRequests and connection establishment

Response time in msec for pages and transactions (a page is a group of requests not separated by a thinktime).

Info »
connect
Mean duration (in msec) of the connection establishment only.
request
Mean duration of requests (in msec).
Info »

Throughput

[% IF async %] [% END %]
TransactionsRequests
Noack/Bidi
Network trafficNew Users

Simultaneous Users

[% IF match %] [% END %]
users
Number of simultaneous users (it's session has started, but not yet finished).
connected
number of users with an opened TCP/UDP connection (example: for HTTP, during a think time, the TCP connection can be closed by the server, and it won't be reopened until the thinktime has expired)
Info » [% IF match %]
[% END %]
Simultaneous UsersMatching responses
[% IF os_mon %]

Server OS monitoring

CPU%Free Memory
CPU Load
[% END %] [% IF http %]

HTTP return code Status (rate)

[% END %] [% IF errors %]

Errors (rate)

[% END %] [% INCLUDE footer.thtml %] tsung-1.8.0/src/templates/footer.thtml0000644000201100017670000000046414377756736017537 0ustar nniclausdream
tsung-1.8.0/src/tsung_recorder/0000755000201100017670000000000014377757020016175 5ustar nniclausdreamtsung-1.8.0/src/tsung_recorder/tsung_recorder.erl0000644000201100017670000000570114377756736021746 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2003 %%% %%% Author : Nicolas Niclausse %%% Created: 22 Dec 2003 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : tsung_recorder.erl %%% Author : %%% Description : tsung_recorder application %%% Created : 22 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(tsung_recorder). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([start/0, start/2, stop/1, stop_all/1]). -behaviour(application). -include("ts_macros.hrl"). %% start the application with it's dependencies start() -> ts_utils:ensure_all_started(tsung_recorder, permanent). %%---------------------------------------------------------------------- %% Func: start/2 %% Returns: {ok, Pid} | %% {ok, Pid, State} | %% {error, Reason} %%---------------------------------------------------------------------- start(_Type, _StartArgs) -> error_logger:tty(false), error_logger:logfile({open, ?config(log_file) ++ "-" ++ atom_to_list(node())}), case ts_recorder_sup:start_link() of {ok, Pid} -> {ok, Pid}; Error -> ?LOGF("Can't start ! ~p ~n",[Error], ?ERR), Error end. %%---------------------------------------------------------------------- %% Func: stop/1 %% Returns: any %%---------------------------------------------------------------------- stop(_State) -> stop. %%---------------------------------------------------------------------- %% Func: stop_all/1 %% Returns: any %%---------------------------------------------------------------------- stop_all(Arg) -> ts_utils:stop_all(Arg,'ts_proxy_listener', "tsung recorder", fun ts_proxy_recorder:stop/1). tsung-1.8.0/src/tsung_recorder/ts_recorder_sup.erl0000644000201100017670000000631714377756736022127 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2003 %%% %%% Author : Nicolas Niclausse %%% Created: 22 Dec 2003 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_recorder_sup.erl %%% Author : %%% Description : %%% Created : 22 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_recorder_sup). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -include("ts_macros.hrl"). -behaviour(supervisor). %% External exports -export([start_link/0]). %% supervisor callbacks -export([init/1]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> ?LOG("starting supervisor ...~n",?INFO), supervisor:start_link({local, ?MODULE}, ?MODULE, []). %%%---------------------------------------------------------------------- %%% Callback functions from supervisor %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, {SupFlags, [ChildSpec]}} | %% ignore | %% {error, Reason} %%---------------------------------------------------------------------- init([]) -> ?LOG("starting",?INFO), ClientsSup = {ts_client_proxy_sup, {ts_client_proxy_sup, start_link, []}, permanent, 2000, supervisor, [ts_client_proxy_sup]}, Recorder = {ts_proxy_recorder, {ts_proxy_recorder, start, [?config(proxy_log_file)]}, transient, 2000, worker, [ts_proxy_recorder]}, Listener = {ts_proxy_listener, {ts_proxy_listener, start, []}, transient, 2000, worker, [ts_proxy_listener]}, {ok,{{one_for_one,?retries,10}, [ClientsSup, Recorder,Listener ]}}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- tsung-1.8.0/src/tsung_recorder/ts_proxy_webdav.erl0000644000201100017670000001401614377756736022137 0ustar nniclausdream%%% %%% Copyright (C) Nicolas Niclausse 2008 %%% %%% Author : Nicolas Niclausse %%% Created: 31 Mar 2008 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_proxy_webdav). -vc('$Id: ts_proxy_webdav.erl 822 2008-03-31 13:18:34Z nniclausse $ '). -author('Nicolas.Niclausse@niclux.org'). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_recorder.hrl"). -export([parse/4, record_request/2, socket_opts/0]). -export([gettype/0]). -export([client_close/2]). -export([rewrite_serverdata/1]). -export([rewrite_ssl/1]). %%-------------------------------------------------------------------- %% Func: socket_opts/0 %%-------------------------------------------------------------------- socket_opts() -> [{packet, 0}]. %%-------------------------------------------------------------------- %% Func: gettype/0 %%-------------------------------------------------------------------- gettype() -> "ts_webdav". %%-------------------------------------------------------------------- %% Func: rewrite_serverdata/1 %%-------------------------------------------------------------------- rewrite_serverdata(Data)-> ts_utils:from_https(Data). %%-------------------------------------------------------------------- %% Func: rewrite_ssl/1 %%-------------------------------------------------------------------- rewrite_ssl(Data)-> ts_utils:to_https(Data). %%-------------------------------------------------------------------- %% Func: client_close/2 %%-------------------------------------------------------------------- client_close(Data,State)-> ts_proxy_http:client_close(Data,State). %%-------------------------------------------------------------------- %% Func: parse/4 %% Purpose: parse HTTP/WEBDAV request %% Returns: {ok, NewState} %%-------------------------------------------------------------------- parse(State,ClientSocket,ServerSocket,String) -> ts_proxy_http:parse(State, ClientSocket,ServerSocket,String). %%-------------------------------------------------------------------- %% Func: record_http_request/2 %% Purpose: record request given State=#state_rec and Request=#http_request %% Returns: {ok, NewState} %%-------------------------------------------------------------------- record_request(State=#state_rec{prev_host=Host, prev_port=Port, prev_scheme=Scheme}, #http_request{method = Method, url = RequestURI, version = HTTPVersion, headers = ParsedHeader,body=Body}) -> FullURL = ts_utils:to_https({url, RequestURI}), {URL,NewPort,NewHost, NewScheme} = case ts_config_http:parse_URL(FullURL) of #url{path=RelURL,host=Host,port=Port,querypart=[],scheme=Scheme}-> {RelURL, Port, Host, Scheme}; #url{path=RelURL,host=Host,port=Port,querypart=Args,scheme=Scheme}-> {RelURL++"?"++Args, Port, Host, Scheme}; #url{host=Host2,port=Port2,scheme=Sc2}-> {FullURL,Port2,Host2,Sc2 } end, Fd = State#state_rec.logfd, URL2 = ts_utils:export_text(URL), io:format(Fd," ok; _ -> Body2 = ts_utils:export_text(Body), io:format(Fd," contents='~s' ", [Body2]) % must be a POST method end, %% Content-type recording (This is useful for SOAP post for example): ts_proxy_http:record_header(Fd,ParsedHeader,"content-type", "content_type='~s' "), ts_proxy_http:record_header(Fd,ParsedHeader,"if-modified-since", "if_modified_since='~s' "), io:format(Fd,"method='~s'>", [Method]), %% authentication ts_proxy_http:record_header(Fd,ParsedHeader,"authorization", "~n "), %% webdav ts_proxy_http:record_header(Fd,ParsedHeader,"depth", "~n ~n"), ts_proxy_http:record_header(Fd,ParsedHeader,"if", "~n ~n"), ts_proxy_http:record_header(Fd,ParsedHeader,"timeout", "~n ~n"), ts_proxy_http:record_header(Fd,ParsedHeader,"overwrite", "~n ~n"), ts_proxy_http:record_header(Fd,ParsedHeader,"destination", "~n ~n", fun(A) -> ts_utils:to_https({url, A}) end), ts_proxy_http:record_header(Fd,ParsedHeader,"url", "~n ~n"), ts_proxy_http:record_header(Fd,ParsedHeader,"lock-token", "~n ~n"), ts_proxy_http:record_header(Fd,ParsedHeader,"x-svn-options", "~n ~n"), %% subversion use x-svn-result-fulltext-md5 ; add this ? %% http://svn.collab.net/repos/svn/branches/artem-soc-work/notes/webdav-protocol io:format(Fd,"~n",[]), {ok,State#state_rec{prev_port=NewPort,prev_host=NewHost,prev_scheme=NewScheme}}. tsung-1.8.0/src/tsung_recorder/ts_proxy_recorder.erl0000644000201100017670000002074314377756736022500 0ustar nniclausdream%%% %%% @copyright IDEALX S.A.S. 2003-2005 %%% %%% @author Nicolas Niclausse %%% @doc Record request by calling the plugin involved %%% @since 1.0.beta1, 22 Dec 2003 by Nicolas Niclausse %%% @version {@version} %%% @end %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_proxy_recorder). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- -include("ts_macros.hrl"). -include("ts_http.hrl"). -include("ts_recorder.hrl"). %%-------------------------------------------------------------------- %% External exports -export([start/1, dorecord/1, recordtag/1, stop/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start/1 %% Description: Starts the server %%-------------------------------------------------------------------- start(Config) -> gen_server:start_link({global, ?MODULE}, ?MODULE, Config, []). %%-------------------------------------------------------------------- %% Function: stop/1 %%-------------------------------------------------------------------- stop(_) -> gen_server:call({global, ?MODULE},{stop}). %%-------------------------------------------------------------------- %% Function: dorecord/1 %% Description: record a new request %%-------------------------------------------------------------------- dorecord(Args)-> gen_server:cast({global, ?MODULE},{record, Args}). %%-------------------------------------------------------------------- %% Function: recordtag/1 %% Description: record a string (for use on the command line) %%-------------------------------------------------------------------- recordtag([Host,Args]) when is_list(Host)-> recordtag(list_to_atom(Host), Args). %% @spec recordtag(Host::string(), Args::term()) -> ok recordtag(Host, Args) when is_list(Args)-> _List = net_adm:world_list([Host]), global:sync(), gen_server:cast({global,?MODULE},{record, Args}). %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init(Filename) -> Date = ts_utils:datestr(), %% add date to filename File = case re:replace(Filename,"\.xml$", Date ++ ".xml", [{return,list},global]) of %% " Filename -> Date ++ "-" ++ Filename; RealName -> RealName end, case file:open(File,[write]) of {ok, Stream} -> Plugin = ?config(plugin), erlang:display(lists:flatten(["Record file: ",File])), ?LOGF("starting recorder with plugin ~s : ~s~n",[Plugin,File],?NOTICE), {ok, #state_rec{ log_file = File, logfd = Stream, ext_file_id=1, plugin = Plugin }}; {error, Reason} -> ?LOGF("Can't open log file ~p! ~p~n",[File,Reason], ?ERR), {stop, Reason} end. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call({stop}, _From, State) -> io:format(State#state_rec.logfd,"~n",[]), file:close(State#state_rec.logfd), {stop, normal, ok, State}; handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast({record, endsession}, State) -> io:format(State#state_rec.logfd,""), {noreply, State}; handle_cast({record, {Request,PluginState}}, State) -> handle_cast({record, {Request}}, State#state_rec{plugin_state=PluginState}); handle_cast({record, {Request}}, State=#state_rec{timestamp=0,plugin=Plugin}) -> % first record Name= ts_utils:datestr(), Type = Plugin:gettype(), io:format(State#state_rec.logfd,"~n",["rec"++Name, Type]), {ok, NewState} = Plugin:record_request(State, Request), {noreply, NewState#state_rec{timestamp=?NOW}}; handle_cast({record, {Request}}, State=#state_rec{plugin=Plugin}) -> TimeStamp=?NOW, Elapsed = ts_utils:elapsed(State#state_rec.timestamp,TimeStamp), case Elapsed < State#state_rec.thinktime_low of true -> ?LOGF("skip too low thinktime, assuming it's an embedded object (~p)~n", [Elapsed],?INFO); false -> io:format(State#state_rec.logfd, "~n~n~n", [round(Elapsed/1000)]) end, {ok, NewState} = Plugin:record_request(State, Request), {noreply, NewState#state_rec{timestamp=TimeStamp}}; handle_cast({record, String}, State) when is_list(String)-> ?LOGF("Record string ~p~n",[String], ?NOTICE), io:format(State#state_rec.logfd, "~n~s~n", [String]), {noreply, State}; handle_cast(Msg, State) -> ?LOGF("IGNORE Msg ~p~n",[Msg], ?WARN), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- tsung-1.8.0/src/tsung_recorder/ts_proxy_pgsql.erl0000644000201100017670000004721114377756736022020 0ustar nniclausdream%%% %%% Copyright (C) Nicolas Niclausse 2005 %%% %%% Author : Nicolas Niclausse %%% Created: 09 Nov 2005 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_proxy_pgsql). -vc('$Id$ '). -author('Nicolas.Niclausse@niclux.org'). -include("ts_macros.hrl"). -include("ts_pgsql.hrl"). -include("ts_recorder.hrl"). -export([parse/4, record_request/2, socket_opts/0, gettype/0]). -export([client_close/2]). -export([rewrite_serverdata/1]). -export([rewrite_ssl/1]). %%-------------------------------------------------------------------- %% Func: socket_opts/0 %%-------------------------------------------------------------------- socket_opts() -> [binary]. %%-------------------------------------------------------------------- %% Func: gettype/0 %%-------------------------------------------------------------------- gettype() -> "ts_pgsql". %%-------------------------------------------------------------------- %% Func: rewrite_serverdata/1 %%-------------------------------------------------------------------- rewrite_serverdata(Data)->{ok, Data}. %%-------------------------------------------------------------------- %% Func: rewrite_ssl/1 %%-------------------------------------------------------------------- rewrite_ssl(Data)->{ok, Data}. %%-------------------------------------------------------------------- %% Func: client_close/2 %%-------------------------------------------------------------------- client_close(_Socket,State)-> ts_proxy_recorder:dorecord({#pgsql_request{type=close}}), State. %%-------------------------------------------------------------------- %% Func: parse/4 %% Purpose: parse PGSQL request %% Returns: {ok, NewState} %%-------------------------------------------------------------------- parse(State=#proxy{parse_status=Status},_,_SSocket,Data= << 0,0,0,8,4,210,22,47 >>) when Status==new -> ?LOG("SSL req: ~n",?DEB), Socket = connect(undefined), ts_client_proxy:send(Socket, Data, ?MODULE), {ok, State#proxy{buffer= << >>,serversock = Socket }}; parse(State=#proxy{parse_status=Status},_,ServerSocket,Data) when Status==new -> <> = Data, ?LOGF("Received data from client: size=~p [~p]~n",[PacketSize, StartupPacket],?DEB), <> = StartupPacket, ?LOGF("Received data from client: proto maj=~p min=~p~n",[ProtoMaj, ProtoMin],?DEB), Res= pgsql_util:split_pair_rec(Data2), case get_db_user(Res) of #pgsql_request{database=undefined} -> ?LOGF("Received data from client: split = ~p~n",[Res],?DEB), Socket = connect(ServerSocket), ts_client_proxy:send(Socket, Data, ?MODULE), {ok, State#proxy{buffer= <<>>, serversock = Socket} }; Req -> ?LOGF("Received data from client: split = ~p~n",[Res],?DEB), ts_proxy_recorder:dorecord({Req#pgsql_request{type=connect}}), Socket = connect(ServerSocket), ts_client_proxy:send(Socket, Data, ?MODULE), {ok, State#proxy{parse_status=open, buffer= <<>>, serversock = Socket} } end; parse(State=#proxy{},_,ServerSocket,Data) -> NewData = << (State#proxy.buffer)/binary, Data/binary >>, ?LOGF("Received data from client: ~p~n",[NewData],?DEB), NewState = process_data(State,NewData), ts_client_proxy:send(ServerSocket, Data, ?MODULE), {ok,NewState}. process_data(State,<< >>) -> State; process_data(State,RawData = <>) -> ?LOGF("PGSQL: received [~p] size=~p Pckt size= ~p ~n",[Code, Size, size(Tail)],?DEB), RealSize = Size-4, case RealSize =< size(Tail) of true -> << Packet:RealSize/binary, Data/binary >> = Tail, NewState=case decode_packet(Code, Packet) of {sql, SQL} -> SQLStr= binary_to_list(SQL), ?LOGF("sql = ~s~n",[SQLStr],?DEB), ts_proxy_recorder:dorecord({#pgsql_request{type=sql, sql=SQLStr}}), State#proxy{buffer= <<>>}; terminate -> ts_proxy_recorder:dorecord({#pgsql_request{type=close}}), State#proxy{buffer= <<>>}; {password, Password} -> PwdStr= binary_to_list(Password), ?LOGF("password = ~s~n",[PwdStr],?DEB), ts_proxy_recorder:dorecord({#pgsql_request{type=authenticate, passwd=PwdStr}}), State#proxy{buffer= <<>>}; {parse,{<< >>,StringQuery,Params} } -> %% TODO: handle Parameters if defined ts_proxy_recorder:dorecord({#pgsql_request{type=parse,equery=StringQuery, parameters=Params}}), State#proxy{buffer= <<>>}; {parse,{StringName,StringQuery,Params} } -> %% TODO: handle Parameters if defined ts_proxy_recorder:dorecord({#pgsql_request{type=parse,name_prepared=StringName, parameters=Params, equery=StringQuery}}), State#proxy{buffer= <<>>}; {bind,{Portal,StringQuery,Params, ParamsFormat,ResFormats} } -> R={#pgsql_request{type=bind, name_prepared=StringQuery, name_portal=Portal, parameters=Params,formats=ParamsFormat, formats_results=ResFormats}}, ts_proxy_recorder:dorecord(R), State#proxy{buffer= <<>>}; {copy, CopyData} -> ts_proxy_recorder:dorecord({#pgsql_request{type=copy,equery=CopyData}}), State#proxy{buffer= <<>>}; copydone -> ts_proxy_recorder:dorecord({#pgsql_request{type=copydone}}), State#proxy{buffer= <<>>}; {copyfail,Msg} -> ts_proxy_recorder:dorecord({#pgsql_request{type=copyfail,equery=Msg}}), State#proxy{buffer= <<>>}; {describe,{<<"S">>,Name} } -> ts_proxy_recorder:dorecord({#pgsql_request{type=describe,name_prepared=Name}}), State#proxy{buffer= <<>>}; {describe,{<<"P">>,Name} } -> ts_proxy_recorder:dorecord({#pgsql_request{type=describe,name_portal=Name}}), State#proxy{buffer= <<>>}; {execute,{NamePortal,Max} } -> ts_proxy_recorder:dorecord({#pgsql_request{type=execute,name_portal=NamePortal,max_rows=Max}}), State#proxy{buffer= <<>>}; sync -> ts_proxy_recorder:dorecord({#pgsql_request{type=sync}}), State#proxy{buffer= <<>>}; flush -> ts_proxy_recorder:dorecord({#pgsql_request{type=flush}}), State#proxy{buffer= <<>>} end, process_data(NewState,Data); false -> ?LOG("need more~n",?DEB), State#proxy{buffer=RawData} end; process_data(State,RawData) -> ?LOG("need more~n",?DEB), State#proxy{buffer=RawData}. get_db_user(Arg) -> get_db_user(Arg,#pgsql_request{}). get_db_user([], Req)-> Req; get_db_user([{"user",User}| Rest], Req)-> get_db_user(Rest,Req#pgsql_request{username=User}); get_db_user([{"database",DB}| Rest], Req) -> get_db_user(Rest,Req#pgsql_request{database=DB}); get_db_user([_| Rest], Req) -> get_db_user(Rest,Req). decode_packet($Q, Data)-> Size= size(Data)-1, <> = Data, {sql, SQL}; decode_packet($p, Data) -> Size= size(Data)-1, <> = Data, {password, Password}; decode_packet($X, _) -> terminate; decode_packet($D, << Type:1/binary, Name/binary >>) -> %describe ?LOGF("Extended protocol: describe ~s ~p~n",[Type,Name], ?DEB), case Name of << 0 >> -> {describe,{Type,[]}}; Bin -> {describe,{Type,Bin}} end; decode_packet($S, _Data) -> %sync sync; decode_packet($H, _Data) -> %flush flush; decode_packet($E, Data) -> %execute {NamePortal,PortalSize} = pgsql_util:to_string(Data), S1=PortalSize+1, << _:S1/binary, MaxParams:32/integer >> = Data, ?LOGF("Extended protocol: execute ~p ~p~n",[NamePortal,MaxParams], ?DEB), case MaxParams of 0 -> {execute,{NamePortal,unlimited}}; Val -> {execute,{NamePortal,Val}} end; decode_packet($d, Data) -> %copy ?LOGF("Extended protocol: copy ~p~n",[Data], ?DEB), {copy, Data}; decode_packet($c, _) -> %copy-complete ?LOG("Extended protocol: copydone~n", ?DEB), copydone; decode_packet($f, Data) -> %copy-fail ?LOGF("Extended protocol: copy failure~p~n", [Data],?DEB), {copyfail, Data}; decode_packet($B, Data) -> %bind [NamePortal, StringQuery | _] = split(Data,<<0>>,[global,trim]), Size = size(NamePortal)+size(StringQuery)+2, << _:Size/binary, NParamsFormat:16/integer,Tail1/binary >> = Data, SizeParamsFormat=2*NParamsFormat, % 16 bits << Formats:SizeParamsFormat/binary, NParams:16/integer, Tail2/binary>> = Tail1, ParamsFormat = case {NParamsFormat,Formats} of {0,_} -> none; {1,<< 0:16/integer >> } -> text; {1,<< 1:16/integer >> } -> binary; _ -> auto end, {Params,<< _NFormatRes:16/integer,FormatsResBin/binary >> }=get_params(NParams,Tail2,[]), ResFormats=get_params_format(FormatsResBin,[]), ?LOGF("Extended protocol: bind ~p ~p ~p ~p ~p~n",[NamePortal,StringQuery,Params,ParamsFormat,ResFormats ], ?DEB), {bind,{NamePortal,StringQuery,Params,ParamsFormat,ResFormats}}; decode_packet($P, Data) -> % parse [StringName, StringQuery | _] = split(Data,<<0>>,[global,trim]), Size = size(StringName)+size(StringQuery)+2, << _:Size/binary, NParams:16/integer,ParamsBin/binary >> = Data, Params=get_params_int(NParams,ParamsBin,[]), ?LOGF("Extended protocol: parse ~p ~p ~p~n",[StringName,StringQuery,Params], ?DEB), {parse,{StringName,StringQuery,Params}}. get_params_format(<<>>,Acc) -> lists:reverse(Acc); get_params_format(<<0:16/integer,Tail/binary>>,Acc) -> get_params_format(Tail,[text|Acc]); get_params_format(<<1:16/integer,Tail/binary>>,Acc) -> get_params_format(Tail,[binary|Acc]). get_params(0,Tail,Acc) -> {lists:reverse(Acc), Tail}; get_params(N,<<-1:32/integer-signed,Tail/binary>>,Acc) -> get_params(N-1,Tail,['null'|Acc]); get_params(N,<>,Acc) -> get_params(N-1,Tail,[S|Acc]). get_params_int(0,_,Acc) -> lists:reverse(Acc); get_params_int(N,<>,Acc) -> get_params_int(N-1,Tail,[Val|Acc]). split(Bin,Pattern,Options)-> binary:split(Bin,Pattern,Options). %%-------------------------------------------------------------------- %% Func: record_request/2 %% Purpose: record request given State=#state_rec and Request=#pgsql_request %% Returns: {ok, NewState} %%-------------------------------------------------------------------- record_request(State=#state_rec{logfd=Fd, plugin_state=connected}, #pgsql_request{type=connect, username=User, database=DB})-> %% connect request while already connected ?LOG("PGSQL: connect request but we are already connected ! record a close request first ~n", ?WARN), io:format(Fd,"~n ~n", []), io:format(Fd,"", [DB,User]), io:format(Fd,"~n",[]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=connect, username=User, database=DB})-> io:format(Fd,"", [DB,User]), io:format(Fd,"~n",[]), {ok,State#state_rec{plugin_state=connected }}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=sql, sql=SQL})-> io:format(Fd," ", [SQL]), io:format(Fd,"~n",[]), {ok,State}; record_request(State=#state_rec{plugin_state=undefined}, #pgsql_request{type=close})-> %% not connected, don't record {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=close})-> io:format(Fd,"~n", []), {ok,State#state_rec{plugin_state=undefined}}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=sync})-> io:format(Fd,"~n", []), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=flush})-> io:format(Fd,"~n", []), {ok,State}; record_request(State=#state_rec{logfd=Fd,ext_file_id=Id}, #pgsql_request{type=copy,equery=Bin}) when size(Bin) > 1024-> FileName=ts_utils:append_to_filename(State#state_rec.log_file,".xml","-"++integer_to_list(Id)++".bin"), ok = file:write_file(FileName,Bin), io:format(Fd,"~n", [FileName]), {ok,State#state_rec{ext_file_id=Id+1} }; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=copy,equery=Bin}) -> Str=ts_utils:join(",",binary_to_list(Bin)), io:format(Fd,"~s~n", [Str]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=copyfail,equery=Bin}) -> Str=binary_to_list(Bin), io:format(Fd,"~n", [Str]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=copydone}) -> io:format(Fd,"~n", []), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=describe,name_prepared=undefined,name_portal=Val}) -> io:format(Fd,"~n", [Val]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=describe,name_portal=undefined,name_prepared=Val}) -> io:format(Fd,"~n", [Val]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=parse,name_portal=undefined, name_prepared=undefined,equery=Query,parameters=Params})-> ParamsStr=ts_utils:join(",",Params), io:format(Fd,"~n", [Query,ParamsStr]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=parse,name_portal=undefined, name_prepared=Val,equery=Query,parameters=Params})-> ParamsStr=ts_utils:join(",",Params), io:format(Fd,"~n", [Val,ParamsStr,Query]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=parse,name_portal=Portal, parameters=Params, name_prepared=Prep,equery=Query})-> ParamsStr=ts_utils:join(",",Params), io:format(Fd,"~n", [Portal,Prep,ParamsStr,Query]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=bind,name_portal = <<>>,name_prepared=Val, parameters=[],formats=ParamsFormat, formats_results=ResFormats})-> ResFormatsStr=ts_utils:join(",",ResFormats), io:format(Fd,"~n", [Val,ParamsFormat,ResFormatsStr]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=bind, name_portal=Portal, name_prepared=Prep, parameters=Params, formats=ParamsFormat, formats_results=ResFormats})-> ParamsStr=ts_utils:join(",",Params), ResFormatsStr=ts_utils:join(",",ResFormats), io:format(Fd,"~n", [Portal,Prep,ParamsFormat,ResFormatsStr,ParamsStr]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=execute,name_portal=[],max_rows=unlimited})-> io:format(Fd,"~n", []), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=execute,name_portal=[],max_rows=Max})-> io:format(Fd,"~n", [Max]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type=execute,name_portal=Portal,max_rows=Max})-> io:format(Fd,"~n", [Portal,Max]), {ok,State}; record_request(State=#state_rec{logfd=Fd}, #pgsql_request{type = authenticate , passwd = Pass }) -> Fd = State#state_rec.logfd, io:format(Fd,"", [Pass]), io:format(Fd,"~n",[]), {ok,State}. connect(undefined) -> {ok, Socket} = gen_tcp:connect(?config(pgsql_server),?config(pgsql_port), [{active, once}, {recbuf, ?tcp_buffer}, {sndbuf, ?tcp_buffer} ]++ socket_opts()), ?LOGF("ok, connected ~p~n",[Socket],?DEB), Socket; connect(Socket) -> Socket. tsung-1.8.0/src/tsung_recorder/ts_proxy_listener.erl0000644000201100017670000002067314377756736022522 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2003 %%% %%% Author : Nicolas Niclausse %%% Created: 22 Dec 2003 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_proxy_listener.erl %%% Author : %%% Description : %%% Created : 22 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_proxy_listener). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- -include("ts_macros.hrl"). -include("ts_recorder.hrl"). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% API -export([start/0]). %% Self callbacks -record(state, { plugin, acceptsock, % The socket we are accept()ing at acceptloop_pid, % The PID of the companion process that blocks % in accept(). accept_count = 0 % The number of accept()s done so far. }). %%==================================================================== %% Server and API functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start/0 %% Description: starts a listener process. %%-------------------------------------------------------------------- start()-> gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). %%==================================================================== %% gen_server callback functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server. This is launched from the %% subprocess and should return a state record. The argument %% is a configuration function %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init(_Config) -> State=#state{plugin=?config(plugin)}, activate(State). %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Purpose: The companion process does synchronous calls to %% us every time accept() returns (either as a new socket or an error). %% We get to tell him whether it should continue or stop in the %% return value of the call. We also honor destroy requests from %% , shutting down the whole listener. %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(stop, _From, State) -> case State#state.acceptsock of undefined -> nothing; Socket -> ssl:close(Socket) end, NewState=State#state{acceptsock=undefined}, {stop, normal, ok, NewState}; handle_call({accepted, _Tag, ClientSock}, _From, State) -> ?LOGF("New socket:~p~n", [ClientSock],?DEB), case ts_client_proxy_sup:start_child(ClientSock) of {ok, Pid} -> ?LOGF("New connection from~p~n", [inet:peername(ClientSock)],?INFO), ok = gen_tcp:controlling_process(ClientSock, Pid), ts_client_proxy:set_active(Pid); Error -> ?LOGF("Failed to launch new client ~p~n",[Error],?ERR), gen_tcp:close(ClientSock) end, NumCnx = State#state.accept_count, {reply, continue, State#state{accept_count=NumCnx+1}}; handle_call({accept_error, _Tag, Error}, _From, State) -> ?LOGF("accept() failed ~p~n",[Error],?ERR), case Error of {error, esslaccept} -> %% Someone may be testing the app by trying plain telnets. %% Let go. {reply, continue, State}; _ -> {stop, Error, stop, State} end; handle_call(_, _From, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(_, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, State) -> case State#state.acceptsock of undefined -> nothing; Socket -> gen_tcp:close(Socket) end, ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%==================================================================== %%% Internal functions %%==================================================================== %%-------------------------------------------------------------------- %% Func: do_activate/1 %% Params: State %% Return: NewState %% Description: activates the listener instance described by State %% and returns the new state. If the instance is already active, do %% nothing. %%-------------------------------------------------------------------- activate(State=#state{plugin=Plugin})-> case State#state.acceptsock of undefined -> Portno=?config(proxy_listen_port), Opts = lists:append(Plugin:socket_opts(), [{reuseaddr, true}, {active, false}]), case gen_tcp:listen(Portno, Opts) of {ok, ServerSock} -> {ok, State#state {acceptsock=ServerSock, acceptloop_pid = spawn_link(ts_utils, accept_loop, [self(), unused, ServerSock])}}; {error, Reason} -> io:format("Error when trying to listen to socket: ~p~n",[Reason]), {stop, Reason} end; _ -> %% Already active {ok, State} end. %% Local Variables: %% tab-width:4 %% End: tsung-1.8.0/src/tsung_recorder/ts_proxy_http.erl0000644000201100017670000004371214377756736021653 0ustar nniclausdream%%% %%% Copyright (C) Nicolas Niclausse 2005 %%% %%% Author : Nicolas Niclausse %%% Created: 09 Nov 2005 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_proxy_http). -vc('$Id$ '). -author('Nicolas.Niclausse@niclux.org'). -include("ts_macros.hrl"). -include("ts_http.hrl"). -include("ts_recorder.hrl"). -export([parse/4, record_request/2, socket_opts/0]). -export([decode_basic_auth/1, gettype/0]). -export([client_close/2]). -export([rewrite_serverdata/1]). -export([rewrite_ssl/1]). %% for webdav: -export([record_header/4, record_header/5]). %%-------------------------------------------------------------------- %% Func: socket_opts/0 %%-------------------------------------------------------------------- socket_opts() -> [{packet, 0}]. %%-------------------------------------------------------------------- %% Func: gettype/0 %%-------------------------------------------------------------------- gettype() -> "ts_http". %%-------------------------------------------------------------------- %% Func: rewrite_serverdata/1 %%-------------------------------------------------------------------- rewrite_serverdata(Data)-> %% FIXME: content length may have changed ! ts_utils:from_https(Data). %%-------------------------------------------------------------------- %% Func: rewrite_ssl/1 %%-------------------------------------------------------------------- rewrite_ssl(Data)-> %% FIXME: content length may have changed ! ts_utils:to_https(Data). %%-------------------------------------------------------------------- %% Func: client_close/2 %%-------------------------------------------------------------------- client_close(_Socket,State)-> State. %%-------------------------------------------------------------------- %% Func: parse/4 %% Purpose: parse HTTP request %% Returns: {ok, NewState} %%-------------------------------------------------------------------- parse(State=#proxy{parse_status=Status, parent_proxy=Parent},_,ServerSocket,NewString) when Status==new -> String = lists:append(State#proxy.buffer,NewString), case ts_http_common:parse_req(String) of {more, _Http, _Head} -> ?LOGF("Headers incomplete (~p), buffering ~n",[String],?DEB), {ok, State#proxy{parse_status=new, buffer=String}}; %FIXME: not optimal {ok, Http=#http_request{url=RequestURI, version=HTTPVersion}, Body} -> ?LOGF("URL ~p ~n",[RequestURI],?DEB), ?LOGF("Method ~p ~n",[Http#http_request.method],?DEB), ?LOGF("Headers ~p ~n",[Http#http_request.headers],?DEB), case ts_utils:key1search(Http#http_request.headers,"content-length") of undefined -> % no body, everything received ts_proxy_recorder:dorecord({Http }), {ok, NewSocket} = check_and_send(String,Parent,ServerSocket,Http,State), case Http#http_request.method of 'CONNECT' -> {ok, State#proxy{http_version=HTTPVersion, parse_status = connect, buffer=[], serversock=NewSocket}}; _ -> {ok, State#proxy{http_version=HTTPVersion, parse_status = new, buffer=[], serversock=NewSocket}} end; Length -> CLength = list_to_integer(Length), ?LOGF("HTTP Content-Length:~p~n",[CLength], ?DEB), BodySize = length(Body), if BodySize == CLength -> % end of response {ok, NewSocket} = check_and_send(String,Parent,ServerSocket,Http,State), ?LOG("End of response, recording~n", ?DEB), ts_proxy_recorder:dorecord({Http#http_request{body=Body}}), {ok, State#proxy{http_version = HTTPVersion, parse_status = new, buffer=[], serversock=NewSocket}}; BodySize > CLength -> {error, bad_content_length}; true -> {ok, NewSocket} = check_and_send(String,Parent,ServerSocket,Http,State), ?LOG("More data to come, continue before recording~n", ?DEB), {ok, State#proxy{http_version=HTTPVersion, content_length = CLength, body_size = BodySize, serversock=NewSocket, buffer = Http#http_request{body=Body }, parse_status = body } } end end end; parse(State=#proxy{parse_status=body, buffer=Http},_,ServerSocket,String) -> DataSize = length(String), ?LOGF("HTTP Body size=~p ~n",[DataSize], ?DEB), Size = State#proxy.body_size + DataSize, CLength = State#proxy.content_length, case ServerSocket of {sslsocket, _, _} -> ts_client_proxy:send(ServerSocket, {body,String}, ?MODULE); _ -> ts_client_proxy:send(ServerSocket, String, ?MODULE) end, Buffer=lists:append(Http#http_request.body,String), %% Should be checked before case Size of CLength -> % end of response ?LOG("End of response, recording~n", ?DEB), ts_proxy_recorder:dorecord( {Http#http_request{ body=Buffer }} ), {ok, State#proxy{body_size=0,parse_status=new, content_length=0,buffer=[]}}; _ -> ?LOGF("Received ~p bytes of data, wait for ~p, continue~n", [Size,CLength],?DEB), {ok, State#proxy{body_size = Size, buffer = Http#http_request{body=Buffer}}} end; parse(State=#proxy{parse_status=connect},_,ServerSocket,String) -> ?LOGF("Received data from client: ~s~n",[String],?DEB), ts_client_proxy:send(ServerSocket, String, ?MODULE), {ok, State}. %%-------------------------------------------------------------------- %% Func: check_and_send/5 %%-------------------------------------------------------------------- check_and_send(String,Parent,ServerSocket,#http_request{url=RequestURI},State)-> {NewSocket,RelURL} = check_serversocket(Parent,ServerSocket,RequestURI,State#proxy.clientsock), ?LOGF("Remove server info from url:[ ~p ] [ ~p ] in [ ~p ] ~n", [RequestURI,RelURL,String], ?INFO), {ok, String2} = relative_url(Parent,String,RequestURI,RelURL), %% needed to remove accept-encoding headers in the http request: {ok, RealString} = ts_utils:to_https({request,String2}), ?LOGF("send data to server: ~p ~n",[RealString],?DEB), ts_client_proxy:send(NewSocket,RealString, ?MODULE), {ok, NewSocket}. %%-------------------------------------------------------------------- %% Func: relative_url/4 %%-------------------------------------------------------------------- relative_url(_,"CONNECT"++_Tail,_RequestURI,[])-> {ok, []}; relative_url(true,String,_RequestURI,_RelURL)-> {ok, String}; relative_url(false,String,RequestURI,RelURL)-> [FullURL_noargs|_] = string:tokens(RequestURI,"?"), [RelURL_noargs|_] = string:tokens(RelURL,"?"), FullURL = re:replace(FullURL_noargs,"(\\)|\\()","\\\\&",[global,{return,list}]), RealString = re:replace(String,FullURL,RelURL_noargs,[{return,list}]), {ok, RealString}. %%-------------------------------------------------------------------- %% Func: check_serversocket/4 %% Purpose: If the socket is not defined, or if the server is not the %% same, connect to the server as specified in URL %% Check if we use a parent proxy, otherwise use check_serversocket/3 %% Returns: {Socket, URL (String)} %%-------------------------------------------------------------------- check_serversocket(false, Socket, URL , ClientSock) -> check_serversocket(Socket, URL , ClientSock); check_serversocket(true, Socket, "http://-"++URL, ClientSock) -> check_serversocket(true, Socket, "https://"++URL, ClientSock); check_serversocket(true, undefined, URL, _ClientSock) -> ?LOGF("Connecting to parent proxy ~p:~p ...~n", [?config(pgsql_server),?config(pgsql_port)],?WARN), {ok ,Socket} = connect(http,?config(pgsql_server),?config(pgsql_port)), {Socket,URL}; check_serversocket(true, Socket, URL, _ClientSock) -> {Socket,URL}. %%-------------------------------------------------------------------- %% Func: check_serversocket/3 %% Purpose: If the socket is not defined, or if the server is not the %% same, connect to the server as specified in URL %% Returns: {Socket, RelativeURL (String)} %%-------------------------------------------------------------------- check_serversocket(Socket, "http://-" ++ Rest, ClientSock) -> check_serversocket(Socket, ts_config_http:parse_URL("https://"++Rest), ClientSock); check_serversocket(Socket, URL, ClientSock) when is_list(URL)-> check_serversocket(Socket, ts_config_http:parse_URL(URL), ClientSock); check_serversocket(undefined, URL = #url{}, ClientSock) -> Port = ts_config_http:set_port(URL), ?LOGF("Connecting to ~p:~p ...~n", [URL#url.host, Port],?DEB), {ok, Socket} = connect(URL#url.scheme, URL#url.host,Port), ?LOGF("Connected to server ~p on port ~p (socket is ~p)~n", [URL#url.host,Port,Socket],?INFO), case URL#url.scheme of connect -> ?LOGF("CONNECT: Send 'connection established' to client socket (~p)",[ClientSock],?DEB), ts_client_proxy:send(ClientSock, "HTTP/1.0 200 Connection established\r\nProxy-agent: tsung\r\n\r\n", ?MODULE), { Socket, [] }; _ -> {Socket, url_with_query(URL)} end; check_serversocket(Socket, URL=#url{host=Host}, _ClientSock) -> RealPort = ts_config_http:set_port(URL), {ok, RealIP} = inet:getaddr(Host,inet), case ts_client_proxy:peername(Socket) of {ok, {RealIP, RealPort}} -> % same as previous URL ?LOGF("Reuse socket ~p on URL ~p~n", [Socket, URL],?DEB), {Socket, url_with_query(URL)}; Other -> ?LOGF("New server configuration (~p:~p, was ~p) on URL ~p~n", [RealIP, RealPort, Other, URL],?DEB), case Socket of {sslsocket, _, _} -> ssl:close(Socket); _ -> gen_tcp:close(Socket) end, {ok, NewSocket} = connect(URL#url.scheme, Host, RealPort), {NewSocket, url_with_query(URL)} end. url_with_query(#url{path=Path, querypart=[]}) -> Path; url_with_query(#url{path=Path, querypart=Query}) -> Path ++"?"++Query. connect(Scheme, Host, Port)-> case Scheme of https -> {ok, _} = ssl:connect(Host,Port, [{active, once}]); _ -> {ok, _} = gen_tcp:connect(Host,Port, [{active, once}, {recbuf, ?tcp_buffer}, {sndbuf, ?tcp_buffer} ]) end. %%-------------------------------------------------------------------- %% Func: record_http_request/2 %% Purpose: record request given State=#state_rec and Request=#http_request %% Returns: {ok, NewState} %%-------------------------------------------------------------------- record_request(State=#state_rec{prev_host=Host, prev_port=Port, prev_scheme=Scheme}, #http_request{method = Method, url = RequestURI, version = HTTPVersion, headers = ParsedHeader,body=Body}) -> FullURL = ts_utils:to_https({url, RequestURI}), {URL,NewPort,NewHost, NewScheme} = case ts_config_http:parse_URL(FullURL) of #url{path=RelURL,host=Host,port=Port,querypart=[],scheme=Scheme}-> {RelURL, Port, Host, Scheme}; #url{path=RelURL,host=Host,port=Port,querypart=Args,scheme=Scheme}-> {RelURL++"?"++Args, Port, Host, Scheme}; #url{host=Host2,port=Port2,scheme=Sc2}-> {FullURL,Port2,Host2,Sc2 } end, Fd = State#state_rec.logfd, URL2 = ts_utils:export_text(URL), io:format(Fd," State#state_rec.ext_file_id; _ -> Id=State#state_rec.ext_file_id, case save_binary_post(ts_utils:key1search(ParsedHeader,"content-type")) of true -> FileName=ts_utils:append_to_filename(State#state_rec.log_file,".xml","-"++integer_to_list(Id)++".bin"), ?LOGF("multipart/form-data, write body data in external binary file ~s~n",[FileName],?NOTICE), ok = file:write_file(FileName,list_to_binary(Body)), io:format(Fd," contents_from_file='~s' ", [FileName]), Id+1; false -> Body2 = ts_utils:export_text(Body), ?LOG("Write body data in XML encoded string ~n",?NOTICE), io:format(Fd," contents='~s' ", [Body2]), Id end end, %% Content-type recording (This is useful for SOAP post for example): record_header(Fd,ParsedHeader,"content-type", "content_type='~s' "), record_header(Fd,ParsedHeader,"if-modified-since", "if_modified_since='~s' "), io:format(Fd,"method='~s'>", [Method]), record_header(Fd,ParsedHeader,"authorization", "~n "), %% SOAP Support: Need to record use of the SOAPAction header record_header(Fd,ParsedHeader,"soapaction", "~n ~n", fun(A) -> string:strip(A,both,$") end ), %" %% Content auto-negotiation headers record_header(Fd,ParsedHeader,"accept", "~n "), record_header(Fd,ParsedHeader,"accept-encoding", "~n "), record_header(Fd,ParsedHeader,"accept-charset", "~n "), record_header(Fd,ParsedHeader,"accept-language", "~n "), record_header(Fd,ParsedHeader,"x-requested-with", "~n "), %% Caching headers record_header(Fd,ParsedHeader,"cache-control", "~n "), record_header(Fd,ParsedHeader,"pragma", "~n "), io:format(Fd,"~n",[]), {ok,State#state_rec{prev_port=NewPort,ext_file_id=NewId,prev_host=NewHost,prev_scheme=NewScheme}}. %% should we save the content of a POST in an external binary file ? save_binary_post("multipart/form-data"++_Tail) -> true; save_binary_post("application/x-amf") -> true; save_binary_post("application/x-silverlight-app") -> true; save_binary_post("application/xaml+xml") -> true; save_binary_post("application/x-ms-xbap") -> true; save_binary_post("application/soap+msbin1") -> true; save_binary_post("application/msbin1") -> true; save_binary_post(_) -> false. %%-------------------------------------------------------------------- %% Func: decode_basic_auth/1 %% Purpose: decode base64 encoded user passwd for basic authentication %% Returns: {User, Passwd} %%-------------------------------------------------------------------- decode_basic_auth(Base64)-> AuthStr= ts_utils:decode_base64(Base64), Sep = string:chr(AuthStr,$:), {string:substr(AuthStr,1,Sep-1),string:substr(AuthStr,Sep+1)}. %%-------------------------------------------------------------------- %% Func: record_header/4 %%-------------------------------------------------------------------- record_header(Fd, Headers, "authorization", Msg)-> %% special case for authorization case ts_utils:key1search(Headers,"authorization") of "Basic " ++ Base64 -> {User,Passwd} = decode_basic_auth(Base64), io:format(Fd, Msg, [User,Passwd]); _ -> ok end; record_header(Fd, Headers, HeaderName, Msg)-> %% record Msg as it is given record_header(Fd, Headers,HeaderName, Msg, fun(A)->A end). %%-------------------------------------------------------------------- record_header(Fd, Headers,HeaderName, Msg, Fun)-> case ts_utils:key1search(Headers,HeaderName) of undefined -> ok; Value -> io:format(Fd,Msg,[Fun(Value)]) end. tsung-1.8.0/src/tsung_recorder/ts_client_proxy_sup.erl0000644000201100017670000000604214377756736023034 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_client_proxy_sup). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behaviour(supervisor). -include("ts_macros.hrl"). %% External exports -export([start_link/0, start_child/1, active_clients/0]). %% supervisor callbacks -export([init/1]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). start_child(Profile) -> supervisor:start_child(?MODULE,[Profile]). %%%---------------------------------------------------------------------- %%% Callback functions from supervisor %%%---------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Func: active_clients/0 %% Returns: [ Client ] %% Description: returns the list of all active children on this beam's %% client supervisor. %%-------------------------------------------------------------------- active_clients()-> length(supervisor:which_children(?MODULE)). %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, {SupFlags, [ChildSpec]}} | %% ignore | %% {error, Reason} %%---------------------------------------------------------------------- init([]) -> ?LOG("Starting ~n", ?INFO), SupFlags = {simple_one_for_one,1, ?restart_sleep}, ChildSpec = [ {ts_client_proxy,{ts_client_proxy, start, []}, temporary,2000,worker,[ts_client_proxy]} ], {ok, {SupFlags, ChildSpec}}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- tsung-1.8.0/src/tsung_recorder/ts_client_proxy.erl0000644000201100017670000002262214377756736022147 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2003 %%% %%% Author : Nicolas Niclausse %%% Created: 22 Dec 2003 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_client_proxy.erl %%% Author : Nicolas Niclausse %%% Description : handle communication with client and server. %%% %%% Created : 22 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_client_proxy). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- -include("ts_macros.hrl"). -include("ts_recorder.hrl"). %%-------------------------------------------------------------------- %% External exports -export([start/1, set_active/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3, peername/1, send/3]). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link/0 %% Description: Starts the gen_server with the socket given by the listener %%-------------------------------------------------------------------- start(Socket) -> gen_server:start_link(?MODULE, [Socket], []). %% tells the client to activate socket set_active(Pid) -> gen_server:cast(Pid, {set_active}). %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init([Socket]) -> ?LOGF("Parent proxy: ~p~n",[?config(parent_proxy)],?DEB), {ok, #proxy{clientsock=Socket, plugin=?config(plugin), parent_proxy=?config(parent_proxy)}}. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast({set_active}, State=#proxy{clientsock=Socket}) -> ts_utils:inet_setopts(tcp, Socket,[{active, once}]), {noreply, State}; handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- % client data, parse and send it to the server. handle_info({tcp, ClientSock, String}, State=#proxy{plugin=Plugin}) when ClientSock == State#proxy.clientsock -> ts_utils:inet_setopts(tcp, ClientSock,[{active, once}]), {ok, NewState} = Plugin:parse(State,ClientSock,State#proxy.serversock,String), {noreply, NewState, ?lifetime}; % server data, send it to the client handle_info({Type, ServerSock, Data}, State=#proxy{plugin=Plugin}) when ServerSock == State#proxy.serversock, ((Type == tcp) or (Type == ssl)) -> ts_utils:inet_setopts(Type, ServerSock,[{active, once}]), ?LOGF("Received data from server: ~s~n",[Data],?DEB), {ok,NewData} = Plugin:rewrite_serverdata(Data), send(State#proxy.clientsock, NewData, Plugin), case re:run(NewData, "[cC]onnection: [cC]lose",[{capture,none}]) of nomatch -> {noreply, State, ?lifetime}; _ -> ?LOG("Connection close received,set close=true~n",?DEB), {noreply, State#proxy{close=true}, ?lifetime} end; %%%%%%%%%%%% Errors and termination %%%%%%%%%%%%%%%%%%% % Log who did close the connection, and exit. handle_info({Msg, Socket}, State = #proxy{ serversock = Socket, close = true }) when Msg==tcp_close; Msg==ssl_closed -> ?LOG("socket closed by server, close client socket also~n",?INFO), {stop, normal, State};% close ask by server in previous request handle_info({Msg,Socket},State=#proxy{http_version = HTTPVersion, serversock = Socket }) when Msg==tcp_close; Msg==ssl_closed -> ?LOG("socket closed by server~n",?INFO), case HTTPVersion of "HTTP/1.0" -> {stop, normal, State};%Disconnect client if it requires HTTP/1.0 _ -> {noreply, State#proxy{serversock=undefined}, ?lifetime} end; handle_info({Msg, Socket}, State=#proxy{plugin=Plugin}) when Msg == tcp_closed; Msg == ssl_closed-> ?LOG("socket closed by client~n",?INFO), NewState = Plugin:client_close(Socket, State), {stop, normal, NewState}; % Log properly who caused an error, and exit. handle_info({Msg, Socket, Reason}, State) when Msg == tcp_error; Msg == ssl_error -> ?LOGF("error on socket ~p ~p~n",[Socket,Reason],?ERR), {stop, {error, sockname(Socket,State), Reason}, State}; handle_info(timeout, State) -> {stop, timeout, State}; handle_info(Info, State) -> ?LOGF("Unknown data ~p~n",[Info],?ERR), {stop, unknown, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, _State) -> % ts_proxy_recorder:dorecord(endsession), ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- %% Function: sockname/2 %% sockname(Socket,State) %% Purpose: decides whether some socket is the client or the server %% Description: State contains two fields, "serversock" and "clientsock". %% This function searches Socket among the two, and returns %% an appropriate atom among 'server', 'client' and 'unknown'. %% Returns: Sockname %% Types: Sockname -> server | client | unknown %% State -> state_record() sockname(Socket,#proxy{serversock=Socket})-> server; sockname(Socket,#proxy{clientsock=Socket})-> client; sockname(_Socket,_State)-> unknown. peername({sslsocket,A,B})-> ssl:peername({sslsocket,A,B}); peername(Socket) -> prim_inet:peername(Socket). send(_,[],_) -> ok; % no data send({sslsocket,A,B},Data, Plugin) -> ?LOGF("Received data to send to an ssl socket ~p, using plugin ~p ~n", [Data,Plugin],?DEB), {ok, RealData } = Plugin:rewrite_ssl({request,Data}), ?LOGF("Sending data to ssl socket ~p ~p (~p)~n", [A, B, RealData],?DEB), ssl:send({sslsocket,A,B}, RealData); send(undefined,_,_) -> ?LOG("No socket ! Error ~n",?CRIT), erlang:error(error_no_socket_open); send(Socket,Data,_) -> gen_tcp:send(Socket,Data). tsung-1.8.0/src/tsung_recorder/tsung_recorder.app.in0000644000201100017670000000145714377756736022355 0ustar nniclausdream{application, tsung_recorder, [{description, "tsung recorder"}, {vsn, "@PACKAGE_VERSION@"}, {modules, [ tsung_recorder, ts_recorder_sup, ts_client_proxy_sup, ts_proxy_recorder, ts_proxy_listener ]}, {registered, [ ts_proxy_recorder, ts_proxy_listener ]}, {env, [ {debug_level, 6}, {ts_cookie, "humhum"}, {log_file, "./tsung.log"}, {plugin, ts_proxy_http}, {parent_proxy, false}, {pgsql_server, "127.0.0.1"}, {pgsql_port, 5432}, {proxy_log_file, "./tsung_recorder"}, {proxy_listen_port, 8090} ]}, {applications, [@ERLANG_APPLICATIONS@]}, {mod, {tsung_recorder, []}} ]}. tsung-1.8.0/src/test/0000755000201100017670000000000014377757020014127 5ustar nniclausdreamtsung-1.8.0/src/test/ipcfg.out0000644000201100017670000000106614377756736015770 0ustar nniclausdream5: eth0 inet 192.12.0.1/32 scope global eth0 5: eth0 inet 192.12.0.2/32 scope global eth0:0 5: eth0 inet 192.12.0.3/32 scope global eth0:1 5: eth0 inet 192.12.0.4/32 scope global eth0:2 5: eth0 inet 192.12.0.5/32 scope global eth0:3 5: eth0 inet 192.12.0.6/32 scope global eth0:4 5: eth0 inet 192.12.0.7/32 scope global eth0:5 5: eth0 inet 192.12.0.8/32 scope global eth0:6 5: eth0 inet 192.12.0.9/32 scope global eth0:7 5: eth0 inet 192.12.0.10/32 scope global eth0:8 5: eth0 inet 192.12.0.11/32 scope global eth0:9 5: eth0 inet 192.12.0.12/32 scope global eth0:10 tsung-1.8.0/src/test/ifcfg.out0000644000201100017670000000425714377756736015763 0ustar nniclausdreameth0 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.183 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:2853444 errors:0 dropped:0 overruns:0 frame:0 TX packets:1157524 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:342116836 (326.2 MiB) TX bytes:147190992 (140.3 MiB) Interrupt:122 Memory:fb000000-fb7fffff eth0:0 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.184 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff eth0:1 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.185 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff eth0:2 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.186 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff eth0:3 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.187 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff eth0:4 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.188 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff eth0:5 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.189 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff eth0:6 Link encap:Ethernet HWaddr 68:B5:99:79:71:5C inet addr:192.168.76.190 Bcast:192.168.79.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 Interrupt:122 Memory:fb000000-fb7fffff tsung-1.8.0/src/test/procnetdev_test.txt0000644000201100017670000000127014377756736020115 0ustar nniclausdreamInter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo:48910337 16323 0 0 0 0 0 0 48910337 16323 0 0 0 0 0 0 eth0:2949197601 10106167 0 0 0 0 0 19182 419719531 2609645 0 0 0 0 0 0 eth1: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 virbr0: 0 0 0 0 0 0 0 0 4012 25 0 0 0 0 0 0 tsung-1.8.0/src/test/procnetdev_test7chars.txt0000644000201100017670000000127114377756736021226 0ustar nniclausdreamInter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo:48910337 16323 0 0 0 0 0 0 48910337 16323 0 0 0 0 0 0 eth0:2949197601 10106167 0 0 0 0 0 19182 419719531 2609645 0 0 0 0 0 0 eth1.14: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 virbr0: 0 0 0 0 0 0 0 0 4012 25 0 0 0 0 0 0 tsung-1.8.0/src/test/netstat_test.txt0000644000201100017670000000055214377756736017430 0ustar nniclausdreamTable d'interfaces noyau Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg wlan0 1500 0 0 0 0 0 0 0 0 0 BMU lo 16436 0 13186 0 0 0 13186 0 0 0 LRU eth0 1500 0 7823989 0 2 0 4272908 0 0 0 BMRU tsung-1.8.0/src/test/netstat_test3.txt0000644000201100017670000000254714377756736017521 0ustar nniclausdreamTable d'interfaces noyau Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg eth0 1500 0 11887768 0 0 0 833748 0 0 0 BMRU eth0:0 1500 0 - no statistics available - BMRU eth0:1 1500 0 - no statistics available - BMRU eth0:3 1500 0 - no statistics available - BMRU eth0:4 1500 0 - no statistics available - BMRU eth0:8 1500 0 - no statistics available - BMRU eth1 1500 0 40621236 0 0 0 40325942 0 0 0 BMRU eth1:0 1500 0 - no statistics available - BMRU eth1:1 1500 0 - no statistics available - BMRU eth1:2 1500 0 - no statistics available - BMRU eth1:3 1500 0 - no statistics available - BMRU eth1:4 1500 0 - no statistics available - BMRU eth2 1500 0 5825149 0 0 0 4149195 0 0 0 BMRU eth3 1500 0 0 0 0 0 4 0 0 0 BMRU lo 16436 0 10530106 0 0 0 10530106 0 0 0 LRU tsung-1.8.0/src/test/netstat_test2.txt0000644000201100017670000000725314377756736017517 0ustar nniclausdreamKernel Interface table Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg bond0 1500 0 35717377985 0 72 0 51928792541 0 0 0 BMmRU eth0 1500 0 35464245354 0 72 0 10222152685 0 0 0 BMsRU eth1 1500 0 84377542 0 0 0 19068087208 0 0 0 BMsRU eth2 1500 0 84377558 0 0 0 12129547888 0 0 0 BMsRU eth3 1500 0 84377531 0 0 0 10509004760 0 0 0 BMsRU eth4 1500 0 1376226016 0 0 0 1065638532 0 0 0 BMRU eth5 1500 0 239281824 0 0 0 159281176 0 0 0 BMRU eth6 1500 0 4354606679 0 0 0 3704530091 0 0 0 BMRU lo 16436 0 19075478 0 0 0 19075478 0 0 0 LRU vif1.0 1500 0 2449883984 0 0 0 4932787016 0 290 0 BMPRU vif1.1 1500 0 20174776 0 0 0 922457211 0 1 0 BMPRU vif11.0 1500 0 1170683857 0 0 0 3728663145 0 405 0 BMPRU vif120.0 1500 0 162318032 0 0 0 195123086 0 275 0 BMPRU vif121.0 1500 0 7035110 0 0 0 86902198 0 61700 0 BMPRU vif13.0 1500 0 189254632 0 0 0 2648494432 0 144 0 BMPRU vif131.0 1500 0 36323244 0 0 0 114665173 0 240 0 BMPRU vif132.0 1500 0 331293668 0 0 0 427660681 0 15522 0 BMPRU vif132.1 1500 0 285434 0 0 0 1572185 0 366 0 BMPRU vif133.0 1500 0 291704054 0 0 0 656316729 0 2669 0 BMPRU vif133.1 1500 0 11 0 0 0 1318276 0 205 0 BMPRU vif134.0 1500 0 1336271 0 0 0 14264970 0 14819 0 BMPRU vif134.1 1500 0 3702136 0 0 0 13180789 0 59 0 BMPRU vif136.0 1500 0 298298 0 0 0 8724792 0 1382 0 BMPRU vif20.0 1500 0 2103005514 0 0 0 4756632990 0 208 0 BMPRU vif20.1 1500 0 2302610336 0 0 0 2988962747 0 135 0 BMPRU vif28.0 1500 0 530670872 0 0 0 2722807108 0 6646 0 BMPRU vif28.1 1500 0 161486331 0 0 0 1233169592 0 46 0 BMPRU vif3.0 1500 0 941732469 0 0 0 3550602523 0 78 0 BMPRU vif30.0 1500 0 31320617 0 0 0 2301508319 0 188 0 BMPRU vif4.0 1500 0 44617622 0 0 0 2528877279 0 14 0 BMPRU vif4.1 1500 0 131 0 0 0 35164655 0 5 0 BMPRU vif5.0 1500 0 1123981715 0 0 0 3672210140 0 47 0 BMPRU vif61.0 1500 0 7886297 0 0 0 627298783 0 306 0 BMPRU vif61.1 1500 0 5595990 0 0 0 287812945 0 4 0 BMPRU vif69.0 1500 0 127685262 0 0 0 979705050 0 443 0 BMPRU vif7.0 1500 0 38828730 0 0 0 2517131705 0 538 0 BMPRU vif7.1 1500 0 1057490859 0 0 0 1368572360 0 3 0 BMPRU vif9.0 1500 0 91364037 0 0 0 2573492550 0 1356 0 BMPRU vif94.0 1500 0 14480917 0 0 0 166584483 0 1137 0 BMPRU xenbr0 1500 0 2378678882 0 0 0 131938 0 0 0 BMRU xenbr1 1500 0 34507916 0 0 0 6 0 0 0 BMRU xenbr2 1500 0 904791170 0 0 0 6 0 0 0 BMRU tsung-1.8.0/src/test/test_file_server_pipe.csv0000644000201100017670000000010414377756736021235 0ustar nniclausdreamconv%2F99%2F589%2Finfo.txt|99|589 conv%2F99%2F938%2Finfo.txt|99|938 tsung-1.8.0/src/test/test_file_server.csv0000644000201100017670000000005514377756736020225 0ustar nniclausdreamusername1;glop; username2;; username3;glop4; tsung-1.8.0/src/test/test_file_server2.csv0000644000201100017670000000001514377756736020303 0ustar nniclausdreamuser1;sesame tsung-1.8.0/src/test/xmpp-muc.xml.in0000644000201100017670000001101414377756736017036 0ustar nniclausdream tsung-1.8.0/src/test/thinkfirst.xml.in0000644000201100017670000000371314377756736017464 0ustar nniclausdream tsung-1.8.0/src/test/badpop.xml.in0000644000201100017670000000306314377756736016542 0ustar nniclausdream tsung-1.8.0/src/test/ts_test_websocket.erl0000644000201100017670000001174214377756736020410 0ustar nniclausdream%%% Author: Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% -module(ts_test_websocket). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -compile(export_all). -include("ts_profile.hrl"). -include("ts_websocket.hrl"). -include("ts_config.hrl"). -include_lib("eunit/include/eunit.hrl"). test()->ok. handshake_test() -> {_, Accept} = websocket:get_handshake("127.0.0.1", "/chat", [], 13, "", []), Response1 = ["HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual(ok, websocket:check_handshake(list_to_binary(Response1), Accept)), Response2 = ["HTTP/1.1 201 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual({error, {mismatch, "Status", "101", "201"}}, websocket:check_handshake(list_to_binary(Response2), Accept)), Response3 = ["HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: anything\r\n\r\n"], ?assertEqual({error, {mismatch, "Sec-WebSocket-Accept", Accept, "anything"}}, websocket:check_handshake(list_to_binary(Response3), Accept)), Response4 = ["HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: anything\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual({error, {mismatch, "Upgrade", "websocket", "anything"}}, websocket:check_handshake(list_to_binary(Response4), Accept)), Response5 = ["HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: anything\r\n", "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual({error, {mismatch, "Connection", "Upgrade", "anything"}}, websocket:check_handshake(list_to_binary(Response5), Accept)), Response6 = ["HTTP/1.1 101 Switching Protocols\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual({error, {miss_headers, "Upgrade"}}, websocket:check_handshake(list_to_binary(Response6), Accept)), Response7 = ["HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n" "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual({error, {miss_headers, "Connection"}}, websocket:check_handshake(list_to_binary(Response7), Accept)), Response8 = ["HTTP/1.1 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n\r\n"], ?assertEqual({error, {miss_headers, "Sec-WebSocket-Accept"}}, websocket:check_handshake(list_to_binary(Response8), Accept)), Response9 = ["HTTP/1.0 101 Switching Protocols\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Sec-WebSocket-Accept: " ++ Accept ++ "\r\n\r\n"], ?assertEqual({error, {mismatch, "Version", "HTTP/1.1", "http/1.0"}}, websocket:check_handshake(list_to_binary(Response9), Accept)). decode_test() -> Data1 = <<16#81,16#05,16#48,16#65,16#6c,16#6c,16#6f>>, {Opcode, Payload, Left} = websocket:decode(Data1), ?assertEqual(?OP_TEXT, Opcode), ?assertEqual(<<"Hello">>, Payload), ?assertEqual(<<>>, Left), Data2 = <<16#81,16#05,16#48,16#65,16#6c,16#6c>>, Result = websocket:decode(Data2), ?assertEqual(more, Result). parse_partial_test() -> Data = <<16#81,16#05,16#48,16#65,16#6c,16#6c >>, Data2 = <<16#6f >>, State = #state_rcv{session=#websocket_session{status=connected}}, {State2,_,_} = ts_websocket:parse(Data, State), ?assertEqual(State#state_rcv{ack_done=false,acc= Data, datasize=size(Data)}, State2), {State3,_,_} = ts_websocket:parse(Data2, State2), ?assertEqual(State#state_rcv{ack_done=true, acc= << >>, datasize=size(Data)+size(Data2)}, State3). myset_env()-> myset_env(0). myset_env(N)-> application:set_env(stdlib, debug_level, N). tsung-1.8.0/src/test/ts_test_utils.erl0000644000201100017670000001075014377756736017560 0ustar nniclausdream%% ========================================================================== %% File : ts_test_utils.erl %% Author : Rodolphe Quiédeville %% Description : %% %% Created : 17 Oct 2013 by Rodolphe Quiédeville %% ========================================================================== -module(ts_test_utils). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). -include_lib("ts_macros.hrl"). test()-> ok. add_time_test() -> ?assertEqual({1382,29907,875287}, ts_utils:add_time({1382,29904,875287},3)), %% have to check this second test, maybe 1383 instead of 1392 Old = {1382,999999,875287}, New = ts_utils:add_time(Old,3), ?assertEqual(3*1000*1000, timer:now_diff(New, Old)). add_elapsed_test() -> T1=?NOW, T2=?NOW, ?assert(ts_utils:elapsed(T1,T2) >= 0). node_to_hostname_test() -> ?assertEqual({ok, "foo"}, ts_utils:node_to_hostname('bar@foo')). to_lower_test()-> ?assertEqual("foo",ts_utils:to_lower("Foo")), ?assertEqual("foo",ts_utils:to_lower("FOO")). mkey1search_atom_test()-> Data = [{foo,bar},{foo,caps},{bar,foo},{foo,caps}], ?assertEqual([bar,caps,caps],ts_utils:mkey1search(Data,foo)). mkey1search_empty_test()-> %% the key does not exists Data = [{foo,bar},{foo,caps},{bar,foo}], ?assertEqual(undefined,ts_utils:mkey1search(Data,foobar)). mkey1search_string_test()-> Data = [{"foo","bar"},{"foo","caps"},{"bar","foo"}], ?assertEqual(["bar","caps"],ts_utils:mkey1search(Data,"foo")). datestr_test()-> ?assertEqual("20131017-1941",lists:flatten(ts_utils:datestr({{2013,10,17},{19,41,29}}))). export_text_test()-> ?assertEqual("foo",ts_utils:export_text("foo")). export_text_bin_test()-> ?assertEqual("foo",ts_utils:export_text(<<"foo">>)). export_text_escape_test()-> ?assertEqual("fo&o",ts_utils:export_text(<<"fo&o">>)), ?assertEqual("A > B",ts_utils:export_text(<<"A > B">>)), ?assertEqual("'B'",ts_utils:export_text("'B'")), ?assertEqual(""B"",ts_utils:export_text("\"B\"")), ?assertEqual("< A",ts_utils:export_text(<<"< A">>)). pack_test()-> Res = ts_utils:pack([node1,node1,node1,node3]), ?assertEqual([[node1,node1,node1],[node3]], Res). pack_single_test()-> Res = ts_utils:pack([node1]), ?assertEqual([[node1]], Res). pack_list_test()-> A=[a,a,a,a,b,c,c,d,d], Res=[[a,a,a,a],[b],[c,c],[d,d]], ?assertEqual(Res, ts_utils:pack(A)). pack_list2_test()-> A=[a,a,a,a,b,c,c,d,d,d], Res=[[a,a,a,a],[b],[c,c],[d,d,d]], ?assertEqual(Res, ts_utils:pack(A)). pack_list3_test()-> A=[a,a,a,a,b,c,c,d,d,d,d,d], Res=[[a,a,a,a],[b],[c,c],[d,d,d,d,d]], ?assertEqual(Res, ts_utils:pack(A)). pack_string_test()-> A=["a","a","a","a","b","c","c","d","d"], Res=[["a","a","a","a"],["b"],["c","c"],["d","d"]], ?assertEqual(Res, ts_utils:pack(A)). pack_dual_test()-> A=[a,b], Res=[[a],[b]], ?assertEqual(Res, ts_utils:pack(A)). pack_singles_test()-> A=[a,b,c,d], Res=[[a],[b],[c],[d]], ?assertEqual(Res, ts_utils:pack(A)). pmap_test()-> F = fun(X) ->X + 1 end, L = [1,2,4,12,6,2,7,9,2,10], Res = lists:map(F,L), ResP = ts_utils:pmap(F,L), ?assertEqual(ResP, Res). pmapn_test()-> F = fun(X) ->X + 1 end, L = [1,2,4,12,6,2,7,9,2,10], Res = lists:map(F,L), ResP = ts_utils:pmap(F,L,3), ?assertEqual(ResP, Res), ResP2 = ts_utils:pmap(F,L,8), ?assertEqual(ResP2, Res). pmapn_big_test()-> F = fun(X) ->X + 1 end, L = lists:duplicate(1000, 42), Res = lists:map(F,L), ResP = ts_utils:pmap(F,L,10), ?assertEqual(ResP, Res). filtermap_test()-> Fun = fun(X) -> case X > 1 of true -> {true, X + 1}; _ -> false end end, ResP = ts_utils:filtermap(Fun, [1,2,3]), Res = [3, 4], ?assertEqual(ResP, Res). spread_list_test()-> A=[a,a,a,a,b,c,c,d], Res=[a,b,c,d,a,c, a,a], ?assertEqual(Res, ts_utils:spread_list(A)). spread_list2_test()-> A=[a,a,a,a,b,c,c,d,d], Res=[a,b,c,d,a,c,d, a,a], ?assertEqual(Res, ts_utils:spread_list(A)). spread_ids_test()-> A = [a,a,a,a,b,c,c,d], SpreadedBeams = ts_utils:spread_list(A), Id0 = 1, {Res,LastId} = lists:mapfoldl(fun(A,Acc) -> {{A, Acc}, Acc+1} end, Id0, SpreadedBeams), Expected = [{a,1},{b,2},{c,3},{d,4},{a,5},{c,6},{a,7},{a,8}], ?assertEqual(Expected, Res). myset_env()-> myset_env(0). myset_env(N)-> application:set_env(stdlib, debug_level, N). tsung-1.8.0/src/test/ts_test_user_server.erl0000644000201100017670000000634614377756736020772 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_user_server.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 20 Mar 2005 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_user_server). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). -include_lib("ts_profile.hrl"). -include_lib("ts_config.hrl"). test()-> ok. next_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(100), ts_user_server:get_idle(), B=ts_user_server:get_idle(), ?assertEqual(2,B). remove_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(100), 1=ts_user_server:get_idle(), B=ts_user_server:get_idle(), ts_user_server:remove_connected(B), C=ts_user_server:get_idle(), ?assertEqual(3,C). full_offline_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(3), 1=ts_user_server:get_idle(), 2=ts_user_server:get_idle(), 3=ts_user_server:get_idle(), ?assertMatch({error,no_free_userid},ts_user_server:get_idle()). full_free_offline_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(3), 1=ts_user_server:get_idle(), B=ts_user_server:get_idle(), 3=ts_user_server:get_idle(), {error,no_free_userid}=ts_user_server:get_idle(), ts_user_server:remove_connected(B), ?assertMatch(B,ts_user_server:get_idle()). full_free_offline_refull_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(3), A=ts_user_server:get_idle(), B=ts_user_server:get_idle(), 3=ts_user_server:get_idle(), {error,no_free_userid}=ts_user_server:get_idle(), ts_user_server:remove_connected(A), ts_user_server:remove_connected(B), B=ts_user_server:get_idle(), ?assertEqual(A,ts_user_server:get_idle()). full_huge_offline_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(1000000), A=ts_user_server:get_idle(), ?assertMatch(2,ts_user_server:get_idle()). offline_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(3), {ok,1}=ts_user_server:get_offline(), {ok,2}=ts_user_server:get_offline(), {ok,3}=ts_user_server:get_offline(), ?assertMatch({ok,1},ts_user_server:get_offline()). offline_full_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(2), ts_user_server:get_idle(), ts_user_server:get_idle(), ?assertMatch({error,no_offline},ts_user_server:get_offline()). online_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(3), A=ts_user_server:get_idle(), B=ts_user_server:get_idle(), ts_user_server:add_to_online(A), ts_user_server:add_to_online(B), ?assertMatch({ok, A},ts_user_server:get_online(B)). online_full_test() -> myset_env(), ts_user_server:start(), ts_user_server:reset(10), A=ts_user_server:get_idle(), B=ts_user_server:get_idle(), ts_user_server:add_to_online(3), ?assertMatch({error,no_online},ts_user_server:get_online(B)). myset_env()-> myset_env(0). myset_env(A)-> application:set_env(stdlib,debug_level,A). tsung-1.8.0/src/test/ts_test_stats.erl0000644000201100017670000000123414377756736017553 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_stats.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 12 Jul 2011 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_stats). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). -include_lib("ts_profile.hrl"). -include_lib("ts_config.hrl"). set_dynvar_random_test() -> Min=1, Max=10, R=lists:map(fun(_)->ts_stats:uniform(Min,Max) end, lists:seq(1,1000)), ?assertEqual(Max,lists:max(R)), ?assertEqual(Min,lists:min(R)). tsung-1.8.0/src/test/ts_test_search.erl0000644000201100017670000005324714377756736017675 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_search.erl %%% Author : Nicolas Niclausse %%% Description : unit tests for ts_search module %%% %%% $Id$ %%%------------------------------------------------------------------- -module(ts_test_search). -compile(export_all). -export([marketplace/1,namespace/1,sessionBucket/1, new/1]). -include_lib("eunit/include/eunit.hrl"). -include_lib("ts_profile.hrl"). -include_lib("ts_config.hrl"). -define(MANY,20). -define(FORMDATA,""). test()-> ok. parse_dyn_var_jsonpath_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [23,45]}", JSONPath = "titi[1]", ?assertEqual([{'myvar',45}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath2_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [23,45]}", JSONPath = "titi[3]", ?assertEqual([{'myvar',<< >>}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath3_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?name=bar].val", ?assertEqual([{'myvar',42}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath4_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?name=void].val", ?assertEqual([{'myvar', << >>}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath5_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"status\": \"foo\"}, {\"val\": 42, \"status\": \"OK\"}, {\"val\": 48, \"status\": \"OK\"}]}", JSONPath = "titi[?status=OK].val", ?assertEqual([{'myvar',[42,48]}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath6_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[*].val", ?assertEqual([{'myvar',[123,42]}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath7_test() -> myset_env(), Data = "\r\n\r\n { \"menu\": { \"id\": \"file\", \"value\": \"File\", \"popup\": { \"name\": \"glop\", \"menuitem\": [ { \"value\": \"New\", \"onclick\": \"CreateNewDoc()\" }, { \"value\": \"Open\", \"onclick\": \"OpenDoc()\" }, { \"value\": \"Close\", \"onclick\": \"CloseDoc()\" } ] } } }", JSONPath = "menu.popup.name", ?assertEqual([{'myvar', << "glop" >>}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))), JSONPathTab = "menu.popup.menuitem[0].value", ?assertEqual([{'myvar', << "New" >>}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPathTab, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath_int_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?val=123].name", ?assertEqual([{'myvar',<<"foo">>}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath_xmpp_test() -> myset_env(), Data="{\n \"status\": \"terminated\",\n \"uid\": \"944370dc04adbee1792732e01097e618af97cc27\",\n \"updated_at\": 1282660758,\n \"nodes\": [\n \"suno-12\",\n \"suno-13\"\n ],\n \"created_at\": 1282660398,\n \"environment\": \"lenny-x64-big\",\n \"result\": {\n \"suno-13\": {\n \"last_cmd_stdout\": \"\",\n \"last_cmd_stderr\": \"\",\n \"cluster\": \"suno\",\n \"ip\": \"192.168.1.113\",\n \"last_cmd_exit_status\": 0,\n \"current_step\": null,\n \"state\": \"OK\"\n },\n \"suno-12\": {\n \"last_cmd_stdout\": \"\",\n \"last_cmd_stderr\": \"\",\n \"cluster\": \"suno\",\n \"ip\": \"192.168.1.112\",\n \"last_cmd_exit_status\": 0,\n \"current_step\": null,\n \"state\": \"OK\"\n }\n },\n \"site_uid\": \"sophia\",\n \"notifications\": [\n \"xmpp:joe@foo.bar/tsung\"\n ],\n \"user_uid\": \"joe\"\n}", JSONPath = "nodes", ?assertMatch([{'nodes',[<<"suno-12">>,<<"suno-13">>]}], ts_search:parse_dynvar([{jsonpath,'nodes', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath_struct_test() -> myset_env(), Data="{\"accessToken\":{\"id\":\"78548a96-cadd-48c0-b7d6-4ff3b81f10cc\",\"lists\":[\"testlist1\"],\"token\":\"rTgdC3f7uJ/Smg3s4b9va2KW5GdPkRHtwYNgWbvwhensgOSf2/wan95VPDiXKnAAGilsZlpw/Td4bs/OPeVeYg==\",\"scope\":[\"GET_ME\",\"WRITE_ACCESS\"]},\"accessTokenSignature\":\"gWAL+zvDcQjqLmNdSwcG/TOWyta5g==\"}", JSONPath = "accessToken", ?assertMatch([{'nodes', << "{\"id\":\"78548a96-cadd-48c0-b7d6-4ff3b81f10cc\",\"lists\":[\"testlist1\"],\"token\":\"rTgdC3f7uJ/Smg3s4b9va2KW5GdPkRHtwYNgWbvwhensgOSf2/wan95VPDiXKnAAGilsZlpw/Td4bs/OPeVeYg==\",\"scope\":[\"GET_ME\",\"WRITE_ACCESS\"]}" >> }], ts_search:parse_dynvar([{jsonpath,'nodes', JSONPath, false} ],list_to_binary(Data))). parse_dyn_var_jsonpath_dynamic_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?name=%%_name%%].val", DynVars = ts_dynvars:new(name, "bar"), ?assertEqual([{'myvar',42}, {'name', "bar"}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, true} ],list_to_binary(Data), DynVars)). parse_dyn_var_jsonpath_dynamic_disabled_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?name=%%_name%%].val", DynVars = ts_dynvars:new(name, "bar"), ?assertEqual([{'myvar', <<>>}, {'name', "bar"}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data), DynVars)). parse_dyn_var_jsonpath_dynamic_enabled_nothing_to_subst_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?name=foo].val", DynVars = ts_dynvars:new(), ?assertEqual([{'myvar',123}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPath, false} ],list_to_binary(Data), DynVars)). parse_dyn_var_xpath_test() -> myset_env(), Data="\r\n\r\n"++?FORMDATA++"", XPath = "//input[@name='jsf_tree_64']/@value", ?assertMatch([{'jsf_tree_64',[<< "H4sIAAAAAAAAAK1VS2/TQBBeo+kalCKAA" >>]}], ts_search:parse_dynvar([{xpath,'jsf_tree_64', XPath} ],list_to_binary(Data))). parse_dyn_var_xpath_with_scripttag_test() -> myset_env(), Data= "\r\n\r\n" ""++?FORMDATA++"", XPath = "//input[@name='jsf_tree_64']/@value", ?assertMatch([{'jsf_tree_64', [<< "H4sIAAAAAAAAAK1VS2/TQBBeo+kalCKAA" >>] }], ts_search:parse_dynvar([{xpath,'jsf_tree_64', XPath} ],list_to_binary(Data))). parse_dyn_var_xpath2_test() -> myset_env(), Data="\r\n\r\n", XPath = "//input[@name='tree64']/@value", ?assertMatch([{tree64,[<< "H4sIAAAAAAAAAK1VS2/TQBBeo+kalCKAA" >>]}], ts_search:parse_dynvar([{xpath,tree64, XPath }],list_to_binary(Data))). parse_dyn_var_xpath3_test() -> myset_env(), Data="\r\n\r\n", XPath = "//hidden[@name='random']/@value", ?assertMatch([{random,[<<"42">>]}], ts_search:parse_dynvar([{xpath, random, XPath }],list_to_binary(Data))). parse_dyn_var_xpath4_test() -> myset_env(), Data="\r\n\r\n/", XPath = "//hidden[@name='random']/@value", ?assertMatch([{random,[<<"42">>]}], ts_search:parse_dynvar([{xpath, random, XPath }],list_to_binary(Data))). parse_dyn_var_many_re_test() -> myset_env(), {Data, Res}= setdata(?MANY,binary), RegexpFun = fun(A) -> {re,list_to_atom(A), ?DEF_RE_DYNVAR_BEGIN++ A ++?DEF_RE_DYNVAR_END} end,%' B=lists:map(fun(A)->"random"++integer_to_list(A) end, lists:seq(1,?MANY)), C=lists:map(RegexpFun, B), {Time, Out}=timer:tc( ts_search,parse_dynvar,[C,list_to_binary(Data)]), erlang:display([?MANY," re:", Time]), ?assertEqual(Res, Out). parse_dyn_var_many_xpath_test() -> myset_env(), {Data, Res}= setdata(?MANY,binarylist), B=lists:map(fun(A)->{xpath, list_to_atom("random"++integer_to_list(A)), "//input[@type='hidden'][@name='random"++integer_to_list(A)++"']/@value"} end, lists:seq(1,?MANY)), {Time, Out}=timer:tc( ts_search,parse_dynvar,[B,list_to_binary(Data)]), erlang:display([?MANY," xpath:", Time]), ?assertMatch(Res, Out). parse_dyn_var_many_xpath_explicit_test() -> myset_env(), {Data, Res}= setdata(?MANY,binarylist), B=lists:map(fun(A)->{xpath, list_to_atom("random"++integer_to_list(A)), "/html/body/form/input[@type='hidden'][@name='random"++integer_to_list(A)++"']/@value"} end, lists:seq(1,?MANY)), {Time, Out}=timer:tc( ts_search,parse_dynvar,[B,list_to_binary(Data)]), erlang:display([?MANY," xpath_explicit:", Time]), ?assertMatch(Res, Out). parse_dyn_var_many_big_re_test() -> myset_env(), {Data, Res}= setdata_big(?MANY,binary), RegexpFun = fun(A) -> {re,list_to_atom(A), ?DEF_RE_DYNVAR_BEGIN++ A ++?DEF_RE_DYNVAR_END} end,%' B=lists:map(fun(A)->"random"++integer_to_list(A) end, lists:seq(1,?MANY)), C=lists:map(RegexpFun, B), {Time, Out}=timer:tc( ts_search,parse_dynvar,[C,list_to_binary(Data)]), erlang:display([?MANY," re_big:", Time]), ?assertMatch(Res, Out). parse_dyn_var_many_big_xpath_test() -> myset_env(), {Data, Res}= setdata_big(?MANY,binarylist), B=lists:map(fun(A)->{xpath, list_to_atom("random"++integer_to_list(A)), "//input[@type='hidden'][@name='random"++integer_to_list(A)++"']/@value"} end, lists:seq(1,?MANY)), {Time, Out}=timer:tc( ts_search,parse_dynvar,[B,list_to_binary(Data)]), erlang:display([?MANY," xpath_big:", Time]), ?assertMatch(Res, Out). parse_dyn_var_many_big_xpath_explicit_test() -> myset_env(), {Data, Res}= setdata_big(?MANY,binarylist), B=lists:map(fun(A)->{xpath, list_to_atom("random"++integer_to_list(A)), "/html/body/form/input[@type='hidden'][@name='random"++integer_to_list(A)++"']/@value"} end, lists:seq(1,?MANY)), {Time, Out}=timer:tc( ts_search,parse_dynvar,[B,list_to_binary(Data)]), erlang:display([?MANY," xpath_explicit_big:", Time]), ?assertMatch(Res, Out). setdata(N) -> setdata(N,list). setdata(N,Type) -> {"\r\n\r\n
"++lists:flatmap(fun(A)-> AI=integer_to_list(A),[""] end,lists:seq(1,N)) ++"
/", lists:reverse(lists:map(fun(A)->{list_to_atom("random"++integer_to_list(A)) , format_result("value"++integer_to_list(A),Type)} end, lists:seq(1,N)))}. setdata_big(N) -> setdata_big(N, list). setdata_big(N, Type) -> Head = "ABCDERFDJSJS", Fields = lists:flatmap(fun(A)-> AI=integer_to_list(A), [""] end,lists:seq(1,N)), Form = "
" ++ Fields ++ "
", Content = "

This is a some random content

" "
  • item1
  • item2
" "

Some more text... not too big really...

" "

More text inside a paragraph element

" "
", HTML ="\r\n\r\n" ++ Head ++ "" ++ Content ++ Content ++ Form ++ Content ++ "", {HTML,lists:reverse(lists:map(fun(A)->{list_to_atom("random"++integer_to_list(A)) , format_result("value"++integer_to_list(A),Type)} end, lists:seq(1,N)))}. format_result(Data,binarylist) -> [list_to_binary(Data)]; format_result(Data,binary) -> list_to_binary(Data); format_result(Data,_) -> Data. parse_re_decode_test() -> myset_env(), Data= << "" >>, StrName="jsf_tree_64", Regexp = ?DEF_RE_DYNVAR_BEGIN++ StrName ++?DEF_RE_DYNVAR_END,%' [{Name,Value}] = ts_search:parse_dynvar([{re, 'jsf_tree_64', Regexp, fun ts_utils:conv_entities/1 }],Data), ?assertEqual("'foo&bar'", ts_search:subst("%%_jsf_tree_64%%",[{Name,Value}])). parse_subst1_re_test() -> myset_env(), Data=?FORMDATA, StrName="jsf_tree_64", Regexp = ?DEF_RE_DYNVAR_BEGIN++ StrName ++?DEF_RE_DYNVAR_END,%' [{Name,Value}] = ts_search:parse_dynvar([{re, 'jsf_tree_64', Regexp }],list_to_binary(Data)), ?assertMatch("H4sIAAAAAAAAAK1VS2/TQBBeo+kalCKAA", ts_search:subst("%%_jsf_tree_64%%",[{Name,Value}])). parse_subst2_re_test() -> myset_env(), Data=" 4DOM version 0.10.2

4DOM version 0.10.2

4Suite http://FourThought.com/4Suite< /A>

", Regexp = "(.*)", [{Name,Value}] = ts_search:parse_dynvar([{re, 'title', Regexp }],list_to_binary(Data)), ?assertMatch("4DOM version 0.10.2", ts_search:subst("%%_title%%",[{Name,Value}])). parse_extract_fun1_test() -> myset_env(), Data="/echo?symbol=%%ts_test_search:new%%", ?assertMatch("/echo?symbol=MSFT", ts_search:subst(Data,[])). parse_extract_fun2_test() -> myset_env(), Data="/stuff/%%ts_test_search:namespace%%/%%ts_test_search:marketplace%%/%%ts_test_search:sessionBucket%%/01/2000?keyA1=dataA1&keyB1=dataB1", ?assertMatch("/stuff/namespace1/5/58/01/2000?keyA1=dataA1&keyB1=dataB1", ts_search:subst(Data,[])). parse_subst_var_fun_test() -> myset_env(), Data=?FORMDATA, StrName="jsf_tree_64", Regexp = ?DEF_RE_DYNVAR_BEGIN++ StrName ++?DEF_RE_DYNVAR_END,%' [{Name,Value}] = ts_search:parse_dynvar([{re, 'jsf_tree_64', Regexp }],list_to_binary(Data)), ?assertMatch("H4sIAAAAAAAAAK1VS2/TQBBeo+kalCKAA-MSFT", ts_search:subst("%%_jsf_tree_64%%-%%ts_test_search:new%%",[{Name,Value}])). parse_subst_badregexp_sid_test() -> myset_env(), Data="HTTP/1.1 200 OK\r\nServer: nginx/0.7.65\r\nDate: Fri, 05 Feb 2010 08:13:29 GMT\r\nContent-Type: text/xml; charset=utf-8\r\nConnection: keep-alive\r\nContent-Length: 373\r\n\r\n", Regexp = "sid=\".*?\"", [{Name,Value}] = ts_search:parse_dynvar([{re, sid, Regexp }],list_to_binary(Data)), ?assertEqual({sid,<<"">>},{Name,Value}). parse_subst_regexp_sid_test() -> myset_env(), Data="HTTP/1.1 200 OK\r\nServer: nginx/0.7.65\r\nDate: Fri, 05 Feb 2010 08:13:29 GMT\r\nContent-Type: text/xml; charset=utf-8\r\nConnection: keep-alive\r\nContent-Length: 373\r\n\r\n", Regexp = "sid=\"([^\"]*)\"", [{Name,Value}] = ts_search:parse_dynvar([{re, sid, Regexp }],list_to_binary(Data)), ?assertEqual({sid,<<"5bfd2b59-3144-4e62-993b-d05d2ae3bee9">>},{Name,Value}). dynvars_urandom_test() -> myset_env(), ?assertMatch([<<"qxvmvtglimieyhemzlxc">>],ts_client:set_dynvars(urandom,{string,20},[toto],[],{},[])). dynvars_urandom_neg_test() -> myset_env(), ?assertError(function_clause,ts_client:set_dynvars(urandom,{string,-3},[toto],[],{},[])). dynvars_urandom2_test() -> myset_env(), ?assertMatch([<<"qxvmvtglimieyhemzlxc">>,<<"qxvmvtglimieyhemzlxc">>],ts_client:set_dynvars(urandom,{string,20},[toto,tutu],[],{},[])). dynvars_random_test() -> myset_env(), [String] = ts_client:set_dynvars(random,{string,20},[toto],[],{},[]), ?assertMatch(20,length(binary_to_list(String))). dynvars_random2_test() -> myset_env(), [String,String2] = ts_client:set_dynvars(random,{string,20},[toto,titi],[],{},[]), ?assertMatch({20,20},{length(binary_to_list(String)),length(binary_to_list(String2))}). dynvars_jsonpath_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": 123, \"name\": \"foo\"}, {\"val\": 42, \"name\": \"bar\"}]}", JSONPath = "titi[?name=bar].val", Dynvars=ts_dynvars:new(data,Data), ?assertEqual(42,ts_client:set_dynvars(jsonpath,{JSONPath,data},[toto],Dynvars,{},[])). dynvars_jsonpath2_test() -> myset_env(), Data="{\"accessToken\":{\"id\":\"78548a96-cadd-48c0-b7d6-4ff3b81f10cc\",\"lists\":[\"testlist1\"],\"token\":\"rTgdC3f7uJ/Smg3s4b9va2KW5GdPkRHtwYNgWbvwhensgOSf2/wan95VPDiXKnAAGilsZlpw/Td4bs/OPeVeYg==\",\"scope\":[\"GET_ME\",\"WRITE_ACCESS\"]},\"accessTokenSignature\":\"gWAL+zvDcQjqLmNdSwcG/TOWyta5g==\"}", JSONPath = "accessToken", JSONPath2 = "accessTokenSignature", Dynvars=ts_dynvars:new(data,Data), Res = << "{\"id\":\"78548a96-cadd-48c0-b7d6-4ff3b81f10cc\",\"lists\":[\"testlist1\"],\"token\":\"rTgdC3f7uJ/Smg3s4b9va2KW5GdPkRHtwYNgWbvwhensgOSf2/wan95VPDiXKnAAGilsZlpw/Td4bs/OPeVeYg==\",\"scope\":[\"GET_ME\",\"WRITE_ACCESS\"]}" >>, ?assertEqual(Res,ts_client:set_dynvars(jsonpath,{JSONPath,data},[toto],Dynvars,{},[])), ?assertEqual(<< "gWAL+zvDcQjqLmNdSwcG/TOWyta5g==" >>,ts_client:set_dynvars(jsonpath,{JSONPath2,data},[toto],Dynvars,{},[])). dynvars_jsonpath3_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [42, 666] }", JSONPath = "titi[1]", Dynvars=ts_dynvars:new(data,Data), ?assertEqual(666,ts_client:set_dynvars(jsonpath,{JSONPath,data},[toto],Dynvars,{},[])). dynvars_jsonpath4_test() -> myset_env(), Data="\r\n\r\n{\"titi\": [{\"val\": [ 123, 231 ] , \"name\": \"foo\"}, {\"val\": [ 13, 23 ], \"name\": \"bar\"}]}", JSONPath = "titi[?name=bar].val[0]", Dynvars=ts_dynvars:new(data,Data), ?assertEqual(13,ts_client:set_dynvars(jsonpath,{JSONPath,data},[toto],Dynvars,{},[])). dynvars_file_test() -> myset_env(), ts_file_server:stop(), ts_file_server:start(), ts_file_server:read([{default,"./src/test/test_file_server.csv"}]), ?assertMatch([<<"username1">>,<<"glop">>, << >>],ts_client:set_dynvars(file,{iter,default,<< ";" >>},[],[],{},[])). dynvars_file_pipe_test() -> myset_env(), ts_file_server:stop(), ts_file_server:start(), ts_file_server:read([{default,"./src/test/test_file_server_pipe.csv"}]), ?assertMatch([<<"conv%2F99%2F589%2Finfo.txt">>,<<"99">> ,<<"589">>],ts_client:set_dynvars(file,{iter,default,<< "|" >>},[],[],{},[])). %%TODO: out of order.. %parse_dynvar_xpath_collection_test() -> % myset_env(), % Data="" % " " % "
" % " " % "
" % " " % " ", % XPath = "//img/@src", % Tree = mochiweb_html:parse(list_to_binary(Data)), % R = mochiweb_xpath:execute(XPath,Tree), % erlang:display(R), % Expected = [<<"img0">>,<<"img1">>,<<"img2">>,<<"img3">>,<<"img4">>], % ?assertMatch(Expected, R). parse_dynvar_xpath_single_test() -> myset_env(), Data="" "
" " " "
" " " "
", XPath = "//a/@href", Tree = mochiweb_html:parse(list_to_binary(Data)), R = mochiweb_xpath:execute(XPath,Tree), erlang:display(R), Expected = [<<"/index.html">>], ?assertEqual(Expected, R). filter_re_include_test() -> ?assertEqual(["/toto"], ts_client:filter({ok,["http://toto/", "/toto", "mailto:bidule"]}, {true, "^/.*"})). filter_re_exclude_test() -> ?assertEqual(["http://toto/", "mailto:bidule"], ts_client:filter({ok,["http://toto/", "/toto", "mailto:bidule"]}, {false,"^/.*"})). extract_body_test() -> Data = << "HTTP header\r\nHeader: value\r\n\r\nbody\r\n" >>, ?assertEqual(<< "body\r\n" >>, ts_search:extract_body(Data)). extract_body_nohttp_test() -> Data = << "random\r\nstuff" >>, ?assertEqual(Data, ts_search:extract_body(Data)). badarg_re_test() -> Data = << "Below this line, is 1000 repeated lines">>, Regexp = "is (\\d+) repeated lines", {ok,Regexp2}=re:compile(Regexp), ?assertEqual([{lines, <<"1000">>}], ts_search:parse_dynvar([{re, 'lines', Regexp2 }],Data)). myset_env()-> myset_env(0). myset_env(Level)-> application:set_env(stdlib,debug_level,Level). new({Pid, DynData}) -> "MSFT". marketplace({Pid,DynData}) -> "5". namespace({Pid,DynData}) -> "namespace1". sessionBucket({Pid,DynData}) -> "58". tsung-1.8.0/src/test/ts_test_recorder.erl0000644000201100017670000001006514377756736020224 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_recorder.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 20 Mar 2005 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_recorder). -compile(export_all). -include("ts_http.hrl"). -include("ts_macros.hrl"). -include_lib("eunit/include/eunit.hrl"). -import(ts_http_common,[parse_req/1, parse_req/2]). -define(HTTP_GET_RES,{ok, #http_request{method='GET', version="1.0"}, _}). test()-> ok. parse_http_request_test() -> ?assertMatch(?HTTP_GET_RES, parse_req("GET / HTTP/1.0\r\n\r\n")). parse_http_partial_request_test() -> % ?log("Testing HTTP request parsing, partial first line ", []), {more,H,Res} = parse_req("GET / HTTP/1.0\r"), ?assertMatch(?HTTP_GET_RES, parse_req(H,Res ++ "\n\r\n")). parse_http_partiel_request2_test() -> {more,H,Res} = parse_req("GET / HTTP/1.0\r\n"), ?assertMatch(?HTTP_GET_RES,parse_req(H,Res ++ "\r\n")). parse_http_request3_test() -> Res = parse_req("POST / HTTP/1.0\r\n\r\nmesdata\r\nsdfsdfs\r\n\r\n"), ?assertMatch({ok, #http_request{method='POST', version="1.0"},"mesdata\r\nsdfsdfs\r\n\r\n"},Res). parse_http_request5_test() -> % ?log("Testing HTTP request parsing, POST with content-length ", []), {ok, Http, Body} = parse_req("POST / HTTP/1.0\r\n" ++"Server: www.glop.org\r\n" ++"Content-length: 16\r\n\r\n" ++"mesdata\r\nsdfsdfs\r\n\r\n"), CL = ts_utils:key1search(Http#http_request.headers,"content-length"), ?assertEqual({16, "mesdata\r\nsdfsdfs\r\n\r\n"},{list_to_integer(CL), Body}). parse_http_request6_test() -> % ?log("Testing HTTP request parsing, POST with content-length; partial ", []), {more, Http, Body} = parse_req("POST / HTTP/1.0\r\n" ++"Server: www.glop.org\r\n" ++"Content-le"), Rest = "ngth: 16\r\n\r\n"++"mesdata\r\nsdfsdfs\r\n\r\n", {ok, Http2, Body2} = parse_req(Http,Body ++ Rest), CL = ts_utils:key1search(Http2#http_request.headers,"content-length"), ?assertEqual({16, "mesdata\r\nsdfsdfs\r\n\r\n"},{list_to_integer(CL), Body2}). parse_http_request7_test() -> Req= "GET http://www.niclux.org/ HTTP/1.1\r\nHost: www.niclux.org\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5) Gecko/20041209 Firefox/1.0 (Ubuntu) (Ubuntu package 1.0-2ubuntu4-warty99)\r\nAccept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nAccept-Language: fr-fr,en-us;q=0.7,en;q=0.3\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nProxy-Connection: keep-alive\r\n\r\n", ?assertMatch({ok, #http_request{method='GET', version="1.1"}, []},parse_req(Req)). decode_base64_test()-> Base="QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ?assertEqual({"Aladdin","open sesame"}, ts_proxy_http:decode_basic_auth(Base)). rewrite_http_secure_cookie_test()-> Data="HTTP/1.1 200 OK\r\nSet-Cookie: JSESSIONID=F949C9182402EB74258F43FDC3F3C63F; Path=/; Secure\r\nLocation: https://foo.bar/\r\nContent-Length: 0\r\n\r\n", NewData="HTTP/1.1 200 OK\r\nSet-Cookie: JSESSIONID=F949C9182402EB74258F43FDC3F3C63F; Path=/\r\nLocation: http://-foo.bar/\r\nContent-Length: 0\r\n\r\n", {ok,Res} = ts_utils:from_https(Data), ?assertEqual(list_to_binary(NewData),iolist_to_binary(Res) ). rewrite_http_secure_cookies_test()-> Data="HTTP/1.1 200 OK\r\nSet-Cookie: JSESSIONID=F949C9182402EB74258F43FDC3F3C63F; Path=/; Secure\r\nSet-Cookie: JSESSIONID=32; Path=/foo; Secure\r\nLocation: https://foo.bar/\r\nContent-Length: 0\r\n\r\n", NewData="HTTP/1.1 200 OK\r\nSet-Cookie: JSESSIONID=F949C9182402EB74258F43FDC3F3C63F; Path=/\r\nSet-Cookie: JSESSIONID=32; Path=/foo\r\nLocation: http://-foo.bar/\r\nContent-Length: 0\r\n\r\n", {ok,Res} = ts_utils:from_https(Data), ?assertEqual(list_to_binary(NewData),iolist_to_binary(Res) ). tsung-1.8.0/src/test/ts_test_proxy.erl0000644000201100017670000001715614377756736017610 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_recorder.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 20 June 2007 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_proxy). -compile(export_all). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_http.hrl"). -include("ts_recorder.hrl"). -include_lib("eunit/include/eunit.hrl"). test()-> ok. relative_url_test()-> myset_env(), String= "foo http://www.glop.com/bar/foo.html foo bar", AbsURI="http://www.glop.com/bar/foo.html?toto=bar", RelURL="/bar/foo.html?toto=bar", ?assertMatch({ok,"foo /bar/foo.html foo bar"}, ts_proxy_http:relative_url(false,String,AbsURI,RelURL)). relative_url2_test()-> myset_env(), String= "foo http://www.glop.com/(;-)/foo.html foo bar", AbsURI="http://www.glop.com/(;-)/foo.html?toto=bar", RelURL="/(;-)/foo.html?toto=bar", ?assertMatch({ok,"foo /(;-)/foo.html foo bar"}, ts_proxy_http:relative_url(false,String,AbsURI,RelURL)). rewrite_http_none_test()-> myset_env(), Data="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Content-Length: 30

http://foo.bar/toto1

", ?assertMatch({ok,Data}, ts_utils:from_https(Data)). rewrite_http_test()-> myset_env(), Data="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Content-Length: 30

https://foo.bar/toto

", NewData="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Content-Length: 30

http://-foo.bar/toto

", {ok,Res}=ts_utils:from_https(Data), ?assertEqual(list_to_binary(NewData),iolist_to_binary(Res) ). rewrite_http_location_test()-> myset_env(), Data="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Location: https://foo.bar/ Content-Length: 30

https://foo.bar/toto or https://foo.bar/glop

", NewData="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Location: http://-foo.bar/ Content-Length: 30

http://-foo.bar/toto or http://-foo.bar/glop

", {ok, Res}=ts_utils:from_https(Data), ?assertEqual(list_to_binary(NewData),iolist_to_binary(Res) ). rewrite_http_location_nourl_test()-> myset_env(), Data="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Location: https://foo.bar/ Content-Length: 30 ", NewData="HTTP/1.1 200 OK Server: Apache/2.0.46 (White Box) Location: http://-foo.bar/ Content-Length: 30 ", {ok, Res} = ts_utils:from_https(Data), ?assertEqual(list_to_binary(NewData), iolist_to_binary(Res)). rewrite_http_body_test()-> myset_env(), Data="sqdfqsdflqkfnmqlskfqd http://-foobar.foo42.fr\r\n", NewData="sqdfqsdflqkfnmqlskfqd https://foobar.foo42.fr\r\n", {ok, Res} = ts_utils:to_https({request,{body,Data}}), ?assertEqual(list_to_binary(NewData), iolist_to_binary(Res)). rewrite_http_encode_test()-> myset_env(), Data="GET http://-foobar.foo42.fr/ HTTP/1.1\r\nHost: -foobar.foo42.fr\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\n\r\n", NewData="GET https://foobar.foo42.fr/ HTTP/1.1\r\nHost: foobar.foo42.fr\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\n\r\n", {ok, Res} = ts_utils:to_https({request,Data}), ?assertEqual(list_to_binary(NewData), iolist_to_binary(Res)). rewrite_http_encode2_test()-> myset_env(), Data="GET http://gforge-qualif.foo.fr/ HTTP/1.1\r\nHost: gforge-qualif.foo.fr\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.1.6) Gecko/20100107 Fedora/3.5.6-1.fc12 Firefox/3.5.6\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: fr,en-us;q=0.7,en;q=0.3\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nProxy-Connection: keep-alive\r\nPragma: no-cache\r\nCache-Control: no-cache\r\n\r\n", NewData="GET http://gforge-qualif.foo.fr/ HTTP/1.1\r\nHost: gforge-qualif.foo.fr\r\nUser-Agent: Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.1.6) Gecko/20100107 Fedora/3.5.6-1.fc12 Firefox/3.5.6\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: fr,en-us;q=0.7,en;q=0.3\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 300\r\nProxy-Connection: keep-alive\r\nPragma: no-cache\r\nCache-Control: no-cache\r\n\r\n", {ok, Res} = ts_utils:to_https({request,Data}), ?assertEqual(list_to_binary(NewData), iolist_to_binary(Res)). rewrite_http_encode3_test()-> myset_env(), Data="GET http://-secure.foo.com/ HTTP/1.1\r\nHost: -secure.com\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9) Gecko/20100101 Firefox/4.0b9\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: fr,en-us;q=0.7,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 115\r\nProxy-Connection: keep-alive\r\n\r\n", NewData="GET https://secure.foo.com/ HTTP/1.1\r\nHost: secure.com\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9) Gecko/20100101 Firefox/4.0b9\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: fr,en-us;q=0.7,en;q=0.3\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7\r\nKeep-Alive: 115\r\nProxy-Connection: keep-alive\r\n\r\n", {ok, Res} = ts_utils:to_https({request,Data}), ?assertEqual(list_to_binary(NewData), iolist_to_binary(Res)). rewrite_webdav_test()-> myset_env(), Data = "REPORT /tsung/!svn/vcc/default HTTP/1.1\r\nUser-Agent: SVN/1.4.4 (r25188) neon/0.25.5\r\nConnection: TE\r\nTE: trailers\r\nContent-Length: 172\r\nContent-Type: text/xml\r\nAccept-Encoding: svndiff1;q=0.9,svndiff;q=0.8\r\nAccept-Encoding: gzip\r\nAccept-Encoding: gzip\r\n\r\nhttp://-svn.process-one.net/tsung/trunk/examples", NewData="REPORT /tsung/!svn/vcc/default HTTP/1.1\r\nUser-Agent: SVN/1.4.4 (r25188) neon/0.25.5\r\nConnection: TE\r\nTE: trailers\r\nContent-Length: 172\r\nContent-Type: text/xml\r\nAccept-Encoding: svndiff1;q=0.9,svndiff;q=0.8\r\n\r\nhttps://svn.process-one.net/tsung/trunk/examples", {ok, Res} = ts_utils:to_https({request,Data}), ?assertEqual(list_to_binary(NewData), iolist_to_binary(Res)). rewrite_http_encode_post_test()-> myset_env(), Data="POST http://-foobar.foo42.fr/ HTTP/1.1\r\nHost: -foobar.foo42.fr\r\nAccept-Encoding: gzip,deflate\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,;q=0.7Content-Type: application/x-www-form-urlencoded\r\nContent-Length: 24\r\n\r\nuname=admin&upass=*****", NewData="POST https://foobar.foo42.fr/ HTTP/1.1\r\nHost: foobar.foo42.fr\r\nAccept-Charset: ISO-8859-15,utf-8;q=0.7,;q=0.7Content-Type: application/x-www-form-urlencoded\r\nContent-Length: 24\r\n\r\nuname=admin&upass=*****", {ok,Res}=ts_utils:to_https({request,Data}), ?assertEqual(list_to_binary(NewData),iolist_to_binary(Res)). %% parse_http_test()-> %% myset_env(), %% ?assertMatch({ok,""}, %% ts_proxy_http:parse(State,ClientSocket,Socket,Data)). myset_env(Val)-> application:set_env(stdlib,debug_level,Val). myset_env()-> myset_env(0). tsung-1.8.0/src/test/ts_test_pgsql.erl0000644000201100017670000001477314377756736017557 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_pgsql.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 10 Apr 2008 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_pgsql). -compile(export_all). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_pgsql.hrl"). -include("ts_recorder.hrl"). -include_lib("eunit/include/eunit.hrl"). -define(PARSEBIN,<< 115,99,117,49,0,100,101,99,108,97, 114,101,32,115,99,117,49,32,99,117,114,115,111, 114,32,119,105,116,104,32,104,111,108,100,32,102, 111,114,32,115,101,108,101,99,116,32,98,114,110, 95,99,100,44,32,112,114,101,118,95,112,114,95, 100,116,44,32,99,117,114,114,95,112,114,95,100, 116,44,32,110,101,120,116,95,112,114,95,100,116, 44,32,98,114,110,95,110,109,44,32,98,114,110,95, 97,100,100,114,49,44,32,98,114,110,95,97,100,100, 114,50,44,32,98,114,110,95,97,100,100,114,51,44, 32,99,111,109,112,95,110,109,44,32,99,97,115,104, 95,97,99,44,32,105,98,116,95,103,114,112,95,99, 100,44,32,98,97,110,107,95,99,100,44,32,108,111, 103,95,112,97,116,104,44,32,99,111,95,98,114,110, 95,99,100,32,102,114,111,109,32,32,32,98,114,110, 32,32,119,104,101,114,101,32,98,114,110,46,98, 114,110,95,99,100,32,61,32,36,49,0,0,1,0,0,4,18 >>). test()-> ok. utils_md5_test()-> myset_env(), Password="sesame", User="benchmd5", Salt= << 54,195,212,197 >>, Hash= list_to_binary(["md5967c89f451d1d504a1f02fc69fb65cb5",0]), PacketSize= 4+size(Hash), Bin= <<$p,PacketSize:32/integer, Hash/binary>>, ?assertMatch(Bin, pgsql_proto:encode_message(pass_md5, {User,Password,Salt} ) ). extended_test()-> Data= << 80,0,0,0,75,115,99,117,49,0,100,101,99,108,97,114,101,32,115,99,117,49,32,99,117,114, 115,111,114,32,119,105,116,104,32,104,111,108,100,32,102,111,114,32,115,101,108,101, 99,116,32,67,79,85,78,84,40,42,41,32,102,114,111,109,32,32,32,98,114,46,97,104,32,0,0, 0,83,0,0,0,4 >>, Result=ts_proxy_pgsql:process_data(#proxy{},Data), ?assertMatch(#proxy{}, Result). extended2_test()-> Data = <<66,0,0,0,28, 0, 115,99,117,49,0, 0,1, 0,0, 0,1, 0,0,0,4, 78,68,83,66, 0,1,0,0, 68,0,0,0,6,80,0,69,0,0,0,9,0,0,0,0,0,83,0,0,0,4>>, Result=ts_proxy_pgsql:process_data(#proxy{},Data), ?assertMatch(#proxy{}, Result). extended3_test()-> Data = <<0, 99,117,51,0, 0,10, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,10, 0,0,0,26, 50,48,48,56,45,49,50,45,48,53,32,48,57,58,49,57,58,48,48,46,48,48,48,48,48,48, 0,0,0,26, 50,48,49,49,45,48,56,45,50,51,32,48,56,58,52,55,58,48,48,46,48,48,48,48,48,48, 0,0,0,10, 49,51,52,52,51,55,32,32,32,32, 0,0,0,1, 69, 0,0,0,5, 75,78,32,32,32, 0,0,0,35, 75,79,78,84,69,78,65,32,78,65,83,73,79,78,65,76,32,66,72,68,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32, 0,0,0,1, 89, 0,0,0,1, 89, 255,255,255,255, 0,0,0,5, 75,78,32,32,32, 0,1, 0,0 >>, Result=ts_proxy_pgsql:decode_packet($B,Data), Portal = <<>>, Statement = <<"cu3">>, Params= [<<"2008-12-05 09:19:00.000000">>, <<"2011-08-23 08:47:00.000000">>, <<"134437 ">>, <<"E">>, <<"KN ">>, <<"KONTENA NASIONAL BHD ">>, <<"Y">>, <<"Y">>, 'null', <<"KN ">>], Bind = {bind, {Portal, Statement, Params , auto, [text]}}, ?assertEqual(Bind, Result). extended_parse_test()-> Prep = <<"scu1">>, Query = <<"declare scu1 cursor with hold for select brn_cd, prev_pr_dt, curr_pr_dt, next_pr_dt, brn_nm, brn_addr1, brn_addr2, brn_addr3, comp_nm, cash_ac, ibt_grp_cd, bank_cd, log_path, co_brn_cd from brn where brn.brn_cd = $1">>, Result = ts_proxy_pgsql:decode_packet($P,?PARSEBIN), ?assertMatch({parse,{Prep, Query,[1042]}}, Result). %% {ok,Dev}=file:open("/tmp/toto.erl.log",[write]), %% State=#state_rec{logfd=Dev}, %% Rec = #pgsql_request{type=parse, parameters=[1042], name_prepared=Prep, equery=Query}, %% ?assertMatch({ok,State}, ts_proxy_pgsql:record_request(State,Rec)). encode_parse_test()-> Prep = <<"scu1">>, Query = <<"declare scu1 cursor with hold for select brn_cd, prev_pr_dt, curr_pr_dt, next_pr_dt, brn_nm, brn_addr1, brn_addr2, brn_addr3, comp_nm, cash_ac, ibt_grp_cd, bank_cd, log_path, co_brn_cd from brn where brn.brn_cd = $1">>, Bin=?PARSEBIN, Res= << 80,0,0,0,234,Bin/binary>>, Rep=pgsql_proto:encode_message(parse,{Prep,Query,[1042]}), ?assertEqual(Res,Rep). encode_parse2_test()-> Rep=pgsql_proto:encode_message(parse,{<< >>,<< >>,[]}), ?assertEqual( << 80,0,0,0,8,0,0,0,0 >> ,Rep). subst_parameters_test()-> myset_env(), Proto=#pgsql_session{}, DynVars=ts_dynvars:new(param,"42"), Params=["%%_param%%","1"], Req=ts_pgsql:add_dynparams(true,{DynVars,Proto}, #pgsql_request{type=bind,name_portal="",name_prepared="P0_10", formats=none,formats_results=[text],parameters=Params}, {"pgsql.org",5432,gen_tcp}), Str=[66,0,0,0,30,0,80,48,95,49,48,0,0,0,0,2,0,0,0,2,52,50,0,0,0,1,49,0,1,0,0], {Res,_}=ts_pgsql:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). subst_parameters2_test()-> myset_env(), Proto=#pgsql_session{}, Params=[42,1], Req=ts_pgsql:add_dynparams(true,{[], Proto}, #pgsql_request{type=bind,name_portal="",name_prepared="P0_10", formats=none,formats_results=[text],parameters=Params}, {"pgsql.org",5432,gen_tcp}), Str=[66,0,0,0,30,0,80,48,95,49,48,0,0,0,0,2,0,0,0,2,52,50,0,0,0,1,49,0,1,0,0], {Res,_}=ts_pgsql:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). myset_env()-> myset_env(0). myset_env(Val)-> application:set_env(stdlib,debug_level,Val). tsung-1.8.0/src/test/ts_test_options.erl0000644000201100017670000000127514377756736020115 0ustar nniclausdream%% ts_test_rate.erl %% @author Nicolas Niclausse %% @doc Test for options like rate limiting feature %% created on 2011-03-14 -module(ts_test_options). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). test() -> ok. rate1_test() -> R=10000 div 1000, B=15000, T0={0,0,0}, T1={0,10,0}, P1=14000, Res = ts_client:token_bucket(R,B,0,T0,P1,T1,false), ?assertEqual({1000,0},Res). rate2_test() -> R=10000 div 1000, B=15000, T0={0,0,0}, T1={0,10,0}, T2={0,11,0}, P1=14000, P2=14000, {S2,0} = ts_client:token_bucket(R,B,0,T0,P1,T1,false), Res = ts_client:token_bucket(R,B,S2,T1,P2,T2,false), ?assertEqual({0,300},Res). tsung-1.8.0/src/test/ts_test_mqtt.erl0000644000201100017670000001611214377756736017403 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_test_mqtt). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -compile(export_all). -include("ts_profile.hrl"). -include("ts_mqtt.hrl"). -include("mqtt.hrl"). -include("ts_config.hrl"). -include_lib("eunit/include/eunit.hrl"). encode_connect_test() -> ClientId = "tsung-test-id", PublishOptions = mqtt_frame:set_publish_options([{qos, 0}, {retain, false}]), Will = #will{topic = "will_topic", message = "will_message", publish_options = PublishOptions}, Options = mqtt_frame:set_connect_options([{client_id, ClientId}, {clean_start, true}, {keepalive, 10}, Will]), Message = #mqtt{type = ?CONNECT, arg = Options}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<16,53,0,6,77,81,73,115,100,112,3,6,0,10,0,13,116,115,117,110, 103,45,116,101,115,116,45,105,100,0,10,119,105,108,108,95, 116,111,112,105,99,0,12,119,105,108,108,95,109,101,115,115, 97,103,101>>, EncodedData). get_message_with_default_client_id_test() -> Session = #mqtt_session{}, State = #state_rcv{session = Session}, Req = #mqtt_request{type = connect, will_qos = 0, will_retain = true, keepalive = 10}, {Encoded,_} = ts_mqtt:get_message(Req,State), {#mqtt{type = Type, arg = Args}, _} = mqtt_frame:decode(Encoded), #connect_options{client_id = ClientId} = Args, ?assertEqual(?CONNECT, Type), ?assertMatch({match,_}, re:run(ClientId, "tsung-")). get_message_with_custom_client_id_test() -> Session = #mqtt_session{}, State = #state_rcv{session = Session}, Req = #mqtt_request{type = connect, will_qos = 0, will_retain = true, keepalive = 10, client_id = "custom-client-id"}, {Encoded,_} = ts_mqtt:get_message(Req,State), {#mqtt{type = Type, arg = Args}, _} = mqtt_frame:decode(Encoded), #connect_options{client_id = ClientId} = Args, ?assertEqual(?CONNECT, Type), ?assertEqual("custom-client-id", ClientId). decode_connect_test() -> Data = <<16,53,0,6,77,81,73,115,100,112,3,6,0,10,0,13,116,115,117,110, 103,45,116,101,115,116,45,105,100,0,10,119,105,108,108,95, 116,111,112,105,99,0,12,119,105,108,108,95,109,101,115,115, 97,103,101>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?CONNECT, Type). encode_disconnect_test() -> Message = #mqtt{type = ?DISCONNECT}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<224,0>>, EncodedData). decode_disconnect_test() -> Data = <<224,0>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?DISCONNECT, Type). encode_publish_test() -> Message = #mqtt{id = 1, type = ?PUBLISH, qos = 0, retain = 0, arg = {"test_topic", "test_message"}}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<48,24,0,10,116,101,115,116,95,116,111,112,105,99,116,101,115,116,95,109,101,115,115,97,103,101>>, EncodedData). decode_publish_test() -> Data = <<48,24,0,10,116,101,115,116,95,116,111,112,105,99,116,101,115,116,95,109,101,115,115,97,103,101>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?PUBLISH, Type). encode_subscribe_test() -> Arg = [#sub{topic = "test_topic", qos = 0}], Message = #mqtt{id = 1, type = ?SUBSCRIBE, arg = Arg, qos = 1}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<130,15,0,1,0,10,116,101,115,116,95,116,111,112,105,99,0>>, EncodedData). decode_subscribe_test() -> Data = <<128,15,0,1,0,10,116,101,115,116,95,116,111,112,105,99,0>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?SUBSCRIBE, Type). encode_unsubscribe_test() -> Arg = [#sub{topic = "test_topic"}], Message = #mqtt{id = 1, type = ?UNSUBSCRIBE, arg = Arg}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<160,14,0,1,0,10,116,101,115,116,95,116,111,112,105,99>>, EncodedData). decode_unsubscribe_test() -> Data = <<160,14,0,1,0,10,116,101,115,116,95,116,111,112,105,99>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?UNSUBSCRIBE, Type). encode_puback_test() -> Message = #mqtt{type = ?PUBACK, arg = 1}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<64,2,0,1>>, EncodedData). decode_puback_test() -> Data = <<64,2,0,1>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?PUBACK, Type). encode_ping_test() -> Message = #mqtt{type = ?PINGREQ}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<192,0>>, EncodedData). decode_ping_test() -> Data = <<192,0>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?PINGREQ, Type). encode_pong_test() -> Message = #mqtt{type = ?PINGRESP}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<208,0>>, EncodedData). decode_pong_test() -> Data = <<208,0>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<>>, Left), ?assertEqual(?PINGRESP, Type). more_fixedheader_only_test() -> Data = <<208>>, Result = mqtt_frame:decode(Data), ?assertEqual(more, Result). more_test() -> Data = <<64,2,0>>, Result = mqtt_frame:decode(Data), ?assertEqual(more, Result). left_test() -> Data = <<64,2,0,1,2,3>>, {#mqtt{type = Type}, Left} = mqtt_frame:decode(Data), ?assertEqual(<<2,3>>, Left), ?assertEqual(?PUBACK, Type). myset_env()-> myset_env(0). myset_env(N)-> application:set_env(stdlib, debug_level, N). tsung-1.8.0/src/test/ts_test_mon.erl0000644000201100017670000000711514377756736017212 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_mon.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 24 August 2007 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_mon). -compile(export_all). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_os_mon.hrl"). -include_lib("eunit/include/eunit.hrl"). test()-> ok. munin_data_ok_test()-> myset_env(), %% error because of empty socket in gen_tcp:recv %% FIXME: start a fake tcp server ?assertError(function_clause, ts_os_mon_munin:read_munin_data(undefined,{ok,"glop 100"},[300])). munin_data_nok_test()-> myset_env(), %% error because of empty socket in gen_tcp:recv %% FIXME: start a fake tcp server ?assertError(function_clause, ts_os_mon_munin:read_munin_data(undefined,{ok,"glop %"},[300])). sample_update_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample,[],50), Val2=ts_stats_mon:update_stats(sample,Val,20), ?assertMatch([35.0,450.0,50,20,2,0,0,0],Val2). sample_update_reset_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample,[],50), Val2=ts_stats_mon:update_stats(sample,Val,20), ?assertMatch([0,0,50,20,0,35.0,2,0],ts_stats_mon:reset_stats(Val2)). sample_counter_update_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample_counter,[],10), Val2=ts_stats_mon:update_stats(sample_counter,Val,60), Val3=ts_stats_mon:update_stats(sample_counter,Val2,80), ?assertMatch([35.0,450.0,50,20,2,0,0,80],Val3). sample_counter_reset_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample_counter,[],10), Val2=ts_stats_mon:update_stats(sample_counter,Val,60), Val3=ts_stats_mon:update_stats(sample_counter,Val2,80), ?assertMatch([0,0,50,20,0,35.0,2,80],ts_stats_mon:reset_stats(Val3)). sample_counter_update2_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample_counter,[],10), Val2=ts_stats_mon:update_stats(sample_counter,Val,30), Val3=ts_stats_mon:update_stats(sample_counter,Val2,80), Val4=ts_stats_mon:update_stats(sample_counter,Val3,202), ?assertMatch([64.0,5496.0,122,20,3,0,0,202],Val4). sample_counter_cycle_update_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample_counter,[],10), Val2=ts_stats_mon:update_stats(sample_counter,Val,60), Val3=ts_stats_mon:update_stats(sample_counter,Val2,40), ?assertMatch([50,0,50,50,1,0,0,40],Val3). sample_counter_zero_update_test()-> myset_env(), Val=ts_stats_mon:update_stats(sample_counter,[],10), Val2=ts_stats_mon:update_stats(sample_counter,Val,60), Val3=ts_stats_mon:update_stats(sample_counter,Val2,0), ?assertMatch([50,0,50,50,1,0,0,60],Val3). netstat_test()-> myset_env(), {ok, Lines} = ts_utils:file_to_list("./src/test/netstat_test.txt"), ?assertMatch({7823989,4272908}, ts_os_mon_erlang:get_os_data(packets, {unix, linux},Lines )). netstat2_test()-> myset_env(), {ok, Lines} = ts_utils:file_to_list("./src/test/netstat_test2.txt"), ?assertMatch({41687492504,56858242340}, ts_os_mon_erlang:get_os_data(packets, {unix, linux}, Lines)). netstat3_test()-> myset_env(), {ok, Lines} = ts_utils:file_to_list("./src/test/netstat_test3.txt"), ?assertMatch({58334153,45308889}, ts_os_mon_erlang:get_os_data(packets, {unix, linux}, Lines)). myset_env()-> myset_env(0). myset_env(V)-> application:set_env(stdlib,debug_level,V). tsung-1.8.0/src/test/ts_test_mochi.erl0000644000201100017670000000070514377756736017516 0ustar nniclausdream-module(ts_test_mochi). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). xpath_parse_test() -> Data="\r\n\r\n
", XPath = "//a/@href", Tree = mochiweb_html:parse(list_to_binary(Data)), ?assertEqual({<<"html">>,[], [{<<"body">>,[], [{<<"a">>, [{<<"href">>, <<"/index.html?name=A&value=B&C">>}], [] }] }] }, Tree). tsung-1.8.0/src/test/ts_test_match.erl0000644000201100017670000001151014377756736017507 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_search.erl %%% Author : Nicolas Niclausse %%% Description : unit tests for ts_search module %%% %%% $Id: ts_test_search.erl 904 2008-10-08 08:16:38Z nniclausse $ %%%------------------------------------------------------------------- -module(ts_test_match). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). -include_lib("ts_profile.hrl"). -include_lib("ts_config.hrl"). -define(MAX_COUNT,42). -define(COUNT,5). -define(USER_ID,2). -define(SESSION_ID,1). -define(COUNTS,{5,42,2,1}). test()-> ok. match_abort_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=abort, 'when'=match}],Data, ?COUNTS,[],[])). match_abort_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(0, ts_search:match([#match{regexp="Erreur", do=abort, 'when'=match}],Data, ?COUNTS,[],[])). nomatch_abort_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(0, ts_search:match([#match{regexp="Erreur", do=abort, 'when'=nomatch}],Data, ?COUNTS,[],[])). nomatch_abort_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=abort, 'when'=nomatch}],Data, ?COUNTS,[],[])). nomatch_continue_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=continue, 'when'=nomatch}],Data, ?COUNTS,[],[])). nomatch_continue_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=continue, 'when'=nomatch}],Data, ?COUNTS,[],[])). match_continue_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=continue, 'when'=match}],Data, ?COUNTS,[],[])). match_continue_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=continue, 'when'=match}],Data, ?COUNTS,[],[])). nomatch_loop_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?COUNT+1, ts_search:match([#match{regexp="Erreur", do=loop, max_loop=?COUNT, loop_back=0, sleep_loop=1,'when'=nomatch}],Data, ?COUNTS,[],[])). nomatch_loop_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=loop, max_loop=?COUNT, loop_back=0, sleep_loop=1,'when'=nomatch}],Data, ?COUNTS,[],[])). match_loop_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=loop, max_loop=?COUNT, loop_back=0, sleep_loop=1,'when'=match}],Data, ?COUNTS,[],[])). match_loop_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT+1, ts_search:match([#match{regexp="Erreur", do=loop, max_loop=?COUNT, loop_back=0, sleep_loop=1, 'when'=match}],Data, ?COUNTS,[],[])). nomatch_restart_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?MAX_COUNT, ts_search:match([#match{regexp="Erreur", do=restart, max_restart=?COUNT,'when'=nomatch}],Data, ?COUNTS,[],[])). nomatch_restart_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=restart, max_restart=?COUNT,'when'=nomatch}],Data, ?COUNTS,[],[])). match_restart_ok_test() -> myset_env(), Data="C'est n'est pas une chaine de caractere", ?assertMatch(?COUNT, ts_search:match([#match{regexp="Erreur", do=restart, max_restart=?COUNT,'when'=match}],Data, ?COUNTS,[],[])). match_restart_nok_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?MAX_COUNT, ts_search:match([#match{regexp="Erreur", do=restart, max_restart=?COUNT, 'when'=match}],Data, ?COUNTS,[],[])). match_subst_undef_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="%%_mydynvar%%", do=restart, subst=true, 'when'=match}],Data, ?COUNTS,[],[])). match_subst_undef2_test() -> myset_env(), Data="Ceci est une Erreur", ?assertMatch(?COUNT, ts_search:match([#match{regexp="ttt%%_mydynvar%%", do=restart, subst=true, 'when'=match}],Data, ?COUNTS,[],[])). match_subst_test() -> myset_env(), Data="Ceci est une Erreur ", Dynvar=ts_dynvars:new(mydynvar,"Erreur"), ?assertMatch(?MAX_COUNT, ts_search:match([#match{regexp="%%_mydynvar%%", do=restart, subst=true, 'when'=match}],Data, ?COUNTS,Dynvar,[])). myset_env()-> myset_env(0). myset_env(Level)-> application:set_env(stdlib,debug_level,Level). tsung-1.8.0/src/test/ts_test_jabber.erl0000644000201100017670000003275114377756736017652 0ustar nniclausdream%%% %%% Copyright © Nicolas Niclausse 2007 %%% %%% Author : Nicolas Niclausse %%% Created: 17 Mar 2007 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% -module(ts_test_jabber). -vc('$Id$ '). -author('Nicolas.Niclausse@niclux.org'). -compile(export_all). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_jabber.hrl"). -include_lib("eunit/include/eunit.hrl"). test()->ok. bidi_pingok_test()-> myset_env(), Req=list_to_binary(" "), Resp=list_to_binary(""), State=#state_rcv{}, ?assertEqual({Resp,State,think}, ts_jabber:parse_bidi(Req,State)). bidi_ping_nok_test()-> myset_env(0), Req=list_to_binary(" "), Resp=list_to_binary(""), State=#state_rcv{}, ?assertEqual({nodata,State,think}, ts_jabber:parse_bidi(Req,State)). bidi_subscribeok_test()-> myset_env(), Req=list_to_binary(" Hi dude. "), Resp=list_to_binary(""), State=#state_rcv{}, ?assertMatch({Resp,State,think}, ts_jabber:parse_bidi(Req,State)). bidi_multisubscribeok_test()-> myset_env(), Req=list_to_binary(" Hi dude. Copaing?."), Resp=list_to_binary(""), State=#state_rcv{}, ?assertMatch({Resp,State,think}, ts_jabber:parse_bidi(Req,State)). bidi_multisubscribe_nok_test()-> myset_env(), Req=list_to_binary(" Hi dude. Copaing?."), Resp=list_to_binary(""), State=#state_rcv{}, ?assertMatch({Resp,State,think}, ts_jabber:parse_bidi(Req,State)). bidi_subscribe_nok_test()-> myset_env(), Req=list_to_binary(" Hi dude. "), State=#state_rcv{}, ?assertMatch({nodata,State,think}, ts_jabber:parse_bidi(Req,State)). bidi_nok_test()-> myset_env(), Req=list_to_binary("Alive."), State=#state_rcv{}, ?assertMatch({nodata,State,think}, ts_jabber:parse_bidi(Req,State)). auth_sasl_test()-> myset_env(), Res = << "AGp1bGlldAByMG0zMG15cjBtMzA=" >>, ?assertMatch(Res, ts_jabber_common:auth_sasl("juliet","r0m30myr0m30","PLAIN")). add_dynparams_test()-> ts_user_server:start(), ts_user_server:reset(100), ts_msg_server:start(), Req = #jabber{id=0,prefix="foo",username="foo",passwd="bar",type='connect',domain={domain,"localdomain"}}, {_,Session} = ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(Req#jabber{id=1,username="foo1",passwd="bar1",user_server=default,domain="localdomain"}, ts_jabber:add_dynparams(true,{[],Session},Req,"localhost")). add_dynparams2_test()-> Req = #jabber{id=0,prefix="foo",username="foo",passwd="bar",type='connect', domain={domain,"localdomain"}}, {_,Session} = ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(Req#jabber{id=2,username="foo2",passwd="bar2",user_server=default,domain="localdomain"}, ts_jabber:add_dynparams(true,{[],Session},Req,"localhost")). get_message_test()-> Req = #jabber{id=0,prefix="foo",username="foo",type='auth_set_plain',passwd="bar",domain={domain,"localdomain"},resource="tsung"}, RepOK = <<"foo3tsungbar3" >>, {Rep,_}=ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(RepOK,Rep ). get_message2_test()-> Req = #jabber{id=user_defined,username="foo",type='auth_set_plain',passwd="bar",domain={domain,"localdomain"},resource="tsung"}, RepOK = <<"footsungbar" >>, {Rep,_} = ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(RepOK,Rep). pubsub_unsubscribe_test()-> ts_user_server:reset(1), Req = #jabber{id=0,prefix="foo",username="foo",type='pubsub:unsubscribe',passwd="bar",domain={domain,"localdomain"},dest=random, node="node", pubsub_service="mypubsub", subid="myid",resource="tsung"}, RepOK= << "" >>, {Rep,_}=ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(RepOK,Rep ). connect_legacy_test()-> ts_user_server:reset(1), Req = #jabber{id=0,prefix="foo",username="foo",type='connect',passwd="bar",domain={domain,"localdomain"},resource="tsung", version="legacy"}, RepOK= <<"" >>, {Rep,_} = ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(RepOK,Rep). connect_xmpp_test()-> ts_user_server:reset(1), Req = #jabber{id=0,prefix="foo",username="foo",type='connect',passwd="bar",domain={domain,"localdomain"},resource="tsung", version="1.0"}, RepOK= <<"" >>, {Rep,_} = ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(RepOK,Rep). pubsub_subscribe_test()-> ts_user_server:reset(1), Req = #jabber{id=0,prefix="foo",dest="foo2",username="foo",type='pubsub:subscribe',passwd="bar",domain={domain,"localdomain"},node="node", pubsub_service="mypubsub", resource="tsung"}, RepOK= << "" >>, {Rep,_}=ts_jabber:get_message(Req,#state_rcv{session=#jabber_session{}}), ?assertEqual(RepOK,Rep ). get_online_test()-> ts_user_server:reset(100), Id=ts_user_server:get_idle(), IdOther = ts_user_server:get_idle(), RealId = ts_jabber_common:set_id(IdOther,"tsung","tsung"), ts_user_server:add_to_online(default,RealId), {ok,Offline} = ts_user_server:get_offline(), {ok,Online} = ts_user_server:get_online(Id), ?assertEqual({1,3,2},{Id,Offline,Online} ). get_online_user_test()-> Server="myserver", ts_user_server_sup:start_user_server(list_to_atom("us_" ++Server)), MyServer = global:whereis_name(list_to_atom("us_"++Server)), ts_user_server:reset(MyServer,100), Id=ts_user_server:get_idle(MyServer), IdOther = ts_user_server:get_idle(MyServer), RealId = ts_jabber_common:set_id(IdOther,"tsung","tsung"), ts_user_server:add_to_online(MyServer,RealId), {ok,Offline} = ts_user_server:get_offline(MyServer), {ok,Online} = ts_user_server:get_online(MyServer, Id), ?assertEqual({1,3,2},{Id,Offline,Online} ). get_online_user_defined_test()-> ts_user_server:reset(0), ts_msg_server:stop(), ts_msg_server:start(), User1 = "tsung1", User2 = "tsung2", User3 = "tsung3", Pwd = "sesame", ts_user_server:add_to_connected({User1,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User1, Pwd) ), ts_user_server:add_to_connected({User2,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User2, Pwd) ), ts_user_server:add_to_connected({User3,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User3, Pwd) ), ts_user_server:remove_connected(default, ts_jabber_common:set_id(user_defined,User3, Pwd) ), {ok,Offline}=ts_user_server:get_offline(), Msg = ts_jabber_common:get_message(#jabber{type = 'presence:directed', id=user_defined,username=User1,passwd=Pwd,prefix="prefix", show = "foo", status="mystatus",user_server=default, domain="domain.org"}), Res = "foomystatus", ?assertEqual(Res, binary_to_list(Msg) ). get_offline_user_defined_test()-> ts_user_server:reset(0), User1 = "tsung1", User3 = "tsung3", Pwd = "sesame", ts_user_server:add_to_connected({User1,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User1, Pwd) ), ts_user_server:add_to_connected({User3,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User3, Pwd) ), ts_user_server:remove_connected(default, ts_jabber_common:set_id(user_defined,User3, Pwd) ), Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = offline, user_server=default, domain="domain.org"}), Res = "hello", ?assertEqual(Res, binary_to_list(Msg) ). get_unique_user_defined_test()-> % this test must be run just after get_offline_user_defined_test Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = unique, user_server=default, domain="domain.org"}), Res = "hello", ?assertEqual(Res, binary_to_list(Msg) ). get_unique_test()-> ts_user_server:reset(2), Id=ts_user_server:get_idle(), Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = unique, user_server=default, domain="domain.org"}), Res = "hello", ?assertEqual(Res, binary_to_list(Msg) ). get_random_test()-> ts_user_server:reset(1), Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = random, user_server=default, domain="domain.org"}), Res = "hello", ?assertEqual(Res, binary_to_list(Msg) ). get_random_user_defined_test()-> ts_user_server:reset(0), Id = xmpp, ts_user_server:set_random_fileid(Id), ts_file_server:start(), ts_file_server:read([{default,"./src/test/test_file_server.csv"}, {Id,"./src/test/test_file_server2.csv"} ]), Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = random, user_server=default, domain="domain.org"}), Res = "hello", ?assertEqual(Res, binary_to_list(Msg) ). get_offline_user_defined_offline_test()-> Id = xmpp, ts_user_server:set_offline_fileid(Id), ts_user_server:reset(0), User1 = "tsung1", Pwd = "sesame", ts_user_server:add_to_connected({User1,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User1, Pwd) ), Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = offline, user_server=default, domain="domain.org"}), Res = "hello", ?assertEqual(Res, binary_to_list(Msg) ). get_offline_user_defined_no_offline_test()-> ts_user_server:reset(0), User1 = "user1", Pwd = "sesame", ts_user_server:add_to_connected({User1,Pwd}), ts_user_server:add_to_online(default, ts_jabber_common:set_id(user_defined,User1, Pwd) ), Msg = ts_jabber_common:get_message(#jabber{type = 'chat', prefix="prefix", data="hello", dest = offline, user_server=default, domain="domain.org"}), %% Res = "hello", Res = "", ts_user_server:set_offline_fileid(undefined), ?assertEqual(Res, binary_to_list(Msg) ). myset_env()-> myset_env(0). myset_env(Val)-> application:set_env(stdlib,debug_level,Val). tsung-1.8.0/src/test/ts_test_interaction.erl0000644000201100017670000000271414377756736020740 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_interaction.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 28 Aug 2012 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_interaction). -compile(export_all). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include_lib("eunit/include/eunit.hrl"). test()-> ok. notify_test()-> myset_env(0), ts_interaction_server:start(), ts_interaction_server:send({chat,now()}), ts_interaction_server:notify({'receive',chat,self()}), ts_interaction_server:rcv({chat,now()}), Res = receive Data -> erlang:display(["received 1",Data]), ok after 1000 -> timeout end, ?assertMatch(ok, Res ). notify_to_test()-> myset_env(0), ts_interaction_server:notify({send,chat2,self()}), ts_interaction_server:send({chat2,now()}), Res = receive Data -> erlang:display(["received 2",Data]), ok after 2000 -> timeout end, ?assertMatch(ok, Res ). myset_env()-> myset_env(0). myset_env(Val)-> application:set_env(stdlib,dumpstats_interval,550), application:set_env(stdlib,mon_file,"test-mon.log"), application:set_env(stdlib,debug_level,Val). tsung-1.8.0/src/test/ts_test_http.erl0000644000201100017670000004115614377756736017403 0ustar nniclausdream%%% %%% Copyright © Nicolas Niclausse 2007 %%% %%% Author : Nicolas Niclausse %%% Created: 17 Mar 2007 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% -module(ts_test_http). -vc('$Id: ts_test_jabber.erl 768 2007-11-15 11:01:01Z mremond $ '). -author('Nicolas.Niclausse@niclux.org'). -compile(export_all). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_config.hrl"). -include_lib("eunit/include/eunit.hrl"). test()->ok. ipv6_url_test() -> URL=ts_config_http:parse_URL("http://[2178:2:5:0:28f:0:3]:8080/toto.php?titi=[43]"), ?assertMatch(#url{path="/toto.php",port=8080,host="2178:2:5:0:28f:0:3",scheme=http}, URL). ipv6_url2_test() -> S=ts_config_http:server_to_url(#server{host="2178:2:5:0:28f:0:3",port=80,type=gen_tcp} ), ?assertEqual("http://[2178:2:5:0:28f:0:3]", S). ipv6_url3_test() -> S=ts_config_http:server_to_url(#server{host="[2178:2:5:0:28f:0:3]",port=80,type=gen_tcp} ), ?assertEqual("http://[2178:2:5:0:28f:0:3]", S). ipv6_url4_test() -> S=ts_config_http:server_to_url(#server{host="2178:2:5:0:28f:0:3",port=8080,type=gen_tcp} ), ?assertEqual("http://[2178:2:5:0:28f:0:3]:8080", S). ipv4_url_test() -> URL=ts_config_http:parse_URL("http://127.0.0.1:8080/"), ?assertMatch(#url{path="/",port=8080,host="127.0.0.1",scheme=http}, URL). emptypath_url_test() -> URL=ts_config_http:parse_URL("http://example.com?foo=bar"), ?assertMatch(#url{path="/",host="example.com",scheme=http, querypart="foo=bar"}, URL). subst_url_test() -> DynVars=ts_dynvars:new('image', "/images/my image with spaces.png"), Req=ts_http:subst(true,#http_request{url="%%_image%%"}, DynVars), ?assertEqual("/images/my%20image%20with%20spaces.png", Req#http_request.url). subst_full_url_test() -> myset_env(), URL="http://myserver/%%_path%%", Proto=#http{user_agent="Firefox"}, DynVars=ts_dynvars:new(path,"bidule/truc"), {Req,_}=ts_http:add_dynparams(true,{DynVars, Proto} , #http_request{url=URL}, {"erlang.org",80,ts_tcp}), Str="GET /bidule/truc HTTP/1.1\r\nHost: myserver\r\nUser-Agent: Firefox\r\n\r\n", {Res,_}=ts_http:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). subst_redirect_test()-> myset_env(), URL="%%_redirect%%", Cookie="toto=bar; path=/; domain=erlang.org", Cookies=ts_http_common:add_new_cookie(Cookie,"erlang.org",[]), Proto=#http{session_cookies=Cookies,user_agent="Firefox"}, DynVars=ts_dynvars:new(redirect,"http://erlang.org/bidule/truc"), {Req,_}=ts_http:add_dynparams(true,{DynVars, Proto} , #http_request{url=URL}, {"erlang.org",80,ts_tcp}), Str="GET /bidule/truc HTTP/1.1\r\nHost: erlang.org\r\nUser-Agent: Firefox\r\nCookie: toto=bar\r\n\r\n", {Res,_}=ts_http:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). subst_ipv6_host_test()-> URL="/%%_dynpath%%", Proto=#http{user_agent="Firefox"}, DynVars=ts_dynvars:new(dynpath,"bidule/truc"), Rep=ts_http:add_dynparams(true,{DynVars, Proto}, #http_request{url=URL}, {"2178:2:5:0:28f:0:3",80,ts_tcp6}), {Res,_}=ts_http:get_message(Rep,#state_rcv{}), OK = << "GET /bidule/truc HTTP/1.1\r\nHost: [2178:2:5:0:28f:0:3]\r\nUser-Agent: Firefox\r\n\r\n" >>, ?assertEqual(OK, Res). subst_ipv6_host2_test()-> URL="/%%_dynpath%%", Proto=#http{user_agent="Firefox"}, DynVars=ts_dynvars:new(dynpath,"bidule/truc"), Rep=ts_http:add_dynparams(true,{DynVars, Proto}, #http_request{url=URL}, {"::1",8080,ts_tcp6}), {Res,_}=ts_http:get_message(Rep,#state_rcv{}), OK = << "GET /bidule/truc HTTP/1.1\r\nHost: [::1]:8080\r\nUser-Agent: Firefox\r\n\r\n" >>, ?assertEqual(OK, Res). subst_redirect_proto_test()-> myset_env(), URL="%%_redirect%%", Cookie="toto=bar; path=/; domain=erlang.org", Cookies=ts_http_common:add_new_cookie(Cookie,"erlang.org",[]), Proto=#http{session_cookies=Cookies,user_agent="Firefox"}, DynVars=ts_dynvars:new(redirect,"http://erlang.org/bidule/truc"), Rep=ts_http:add_dynparams(true,{DynVars, Proto}, #http_request{url=URL}, {"erlang.org",80,ts_tcp6}), ?assertMatch({_,{"erlang.org",80,ts_tcp6}}, Rep). subst_cookie_test()-> myset_env(), URL="/bidule/truc", Cookie="bar=%%_foovar%%; path=/; domain=erlang.org", Cookies=ts_http_common:add_new_cookie(Cookie,"erlang.org",[]), Proto=#http{user_agent="Firefox"}, DynVars=ts_dynvars:new(foovar,"foo"), Req=ts_http:add_dynparams(true,{DynVars, Proto}, #http_request{url=URL,cookie=Cookies}, {"erlang.org",80,ts_tcp}), Str="GET /bidule/truc HTTP/1.1\r\nHost: erlang.org\r\nUser-Agent: Firefox\r\nCookie: bar=foo\r\n\r\n", {Res,_}=ts_http:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). cookie_subdomain_test()-> myset_env(), URL="/bidule/truc", Cookie="toto=bar; path=/; domain=.domain.org", Cookies=ts_http_common:add_new_cookie(Cookie,"domain.org",[]), Proto=#http{session_cookies=Cookies,user_agent="Firefox"}, DynVars=ts_dynvars:new(), Req=ts_http:add_dynparams(false,{DynVars, Proto}, #http_request{url=URL}, {"www.domain.org",80,ts_tcp}), Str="GET /bidule/truc HTTP/1.1\r\nHost: www.domain.org\r\nUser-Agent: Firefox\r\nCookie: toto=bar\r\n\r\n", {Res,_}=ts_http:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). cookie_dotdomain_test()-> myset_env(), URL="/bidule/truc", Cookie="toto=bar; path=/; domain=.www.domain.org", Cookies=ts_http_common:add_new_cookie(Cookie,"www.domain.org",[]), Proto=#http{session_cookies=Cookies,user_agent="Firefox"}, DynVars=ts_dynvars:new(), Req=ts_http:add_dynparams(false,{DynVars, Proto}, #http_request{url=URL}, {"www.domain.org",80, ts_tcp}), Str="GET /bidule/truc HTTP/1.1\r\nHost: www.domain.org\r\nUser-Agent: Firefox\r\nCookie: toto=bar\r\n\r\n", {Res,_}=ts_http:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). add_cookie_samekey_samedomain_test()-> myset_env(), Cookie1="RMID=732423sdfs73242; path=/; domain=.example.net", Cookie2="RMID=42; path=/; domain=.example.net", Val1=#cookie{key="RMID",value="732423sdfs73242",domain=".example.net",path="/"}, Val2=#cookie{key="RMID",value="42",domain=".example.net",path="/"}, Cookies=ts_http_common:add_new_cookie(Cookie1,"foobar.com",[]), %% same domain, second cookie should erase the first one Res=ts_http_common:add_new_cookie(Cookie2,"foobar.com",Cookies), ?assertMatch([Val2],Res). add_cookie_replace_key_default_domain_test()-> myset_env(), Cookie1="RMID=732423sdfs73242; path=/; ", Cookie2="RMID=42; path=/; domain=.example.net", Val2=#cookie{key="RMID",value="42",domain=".example.net",path="/"}, Cookies=ts_http_common:add_new_cookie(Cookie1,"example.net",[]), %% same domain, second cookie should erase the first one Res=ts_http_common:add_new_cookie(Cookie2,"foobar.com",Cookies), ?assertEqual([Val2],Res). set_cookie_test()-> myset_env(), Cookie="RMID=732423sdfs73242; path=/; domain=.foobar.com", Val="Cookie: RMID=732423sdfs73242\r\n", Cookies=ts_http_common:add_new_cookie(Cookie,"www.foobar.com",[]), ?assertEqual(Val,lists:flatten(ts_http_common:set_cookie_header({Cookies,"www.foobar.com","/toto.html"}))). add_cookie_test()-> myset_env(), Cookie1="RMID=732423sdfs73242; expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; domain=.example.net", Cookie2="ID=42; path=/; domain=.example.net", Val1=#cookie{key="RMID",value="732423sdfs73242",domain=".example.net",path="/",expires="Fri, 31-Dec-2010 23:59:59 GMT"}, Val2=#cookie{key="ID",value="42",domain=".example.net",path="/"}, Cookies=ts_http_common:add_new_cookie(Cookie1,"foobar.com",[]), ?assertEqual([Val2,Val1],ts_http_common:add_new_cookie(Cookie2,"foobar.com",Cookies)). add_cookie_samekey_nodomain_test()-> myset_env(), Cookie1="RMID=732423sdfs73242; expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; domain=.example.net", Cookie2="RMID=42; path=/; domain=.foobar.net", Val1=#cookie{key="RMID",value="732423sdfs73242",domain=".example.net",path="/",expires="Fri, 31-Dec-2010 23:59:59 GMT"}, Val2=#cookie{key="RMID",value="42",domain=".foobar.net",path="/"}, Cookies=ts_http_common:add_new_cookie(Cookie1,"foobar.com",[]), %% two different domains, two cookies ?assertEqual([Val2,Val1],ts_http_common:add_new_cookie(Cookie2,"foobar.com",Cookies)). add_cookie_samekey_nodomain_req_test()-> myset_env(), URL="/bidule/truc", Cookie1="RMID=732423sdfs73242; expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; domain=.example.net", Cookie2="RMID=42; path=/; domain=.foobar.net", Cookies1=ts_http_common:add_new_cookie(Cookie1,"",[]), Cookies = ts_http_common:add_new_cookie(Cookie2,"",Cookies1), Proto=#http{session_cookies=Cookies,user_agent="Firefox"}, DynVars=ts_dynvars:new(), Req=ts_http:add_dynparams(false,{DynVars, Proto}, #http_request{url=URL}, {"www.foobar.net",80, ts_tcp}), Str="GET /bidule/truc HTTP/1.1\r\nHost: www.foobar.net\r\nUser-Agent: Firefox\r\nCookie: RMID=42\r\n\r\n", {Res,_}=ts_http:get_message(Req,#state_rcv{}), ?assertEqual(Str, binary_to_list(Res)). chunk_header_ok1_test()-> Rep=ts_http_common:parse_line("transfer-encoding: chunked\r\n",#http{},[]), ?assertMatch(#http{chunk_toread=0}, Rep). chunk_header_ok2_test()-> Rep=ts_http_common:parse_line("transfer-encoding: Chunked\r\n",#http{},[]), ?assertMatch(#http{chunk_toread=0}, Rep). chunk_header_ok3_test()-> Rep=ts_http_common:parse_line("transfer-encoding:chunked\r\n",#http{},[]), ?assertMatch(#http{chunk_toread=0}, Rep). chunk_header_bad_test()-> Rep=ts_http_common:parse_line("transfer-encoding: cheddar\r\n",#http{},[]), ?assertMatch(#http{chunk_toread=-1}, Rep). parse_304_test() -> Res = <<"HTTP/1.1 304 Not Modified\r\nDate: Fri, 24 Aug 2012 07:49:37 GMT\r\nServer: Apache/2.2.16 (Debian)\r\nETag: \"201ad-10fb-473ae23fb0600\"\r\nVary: Accept-Encoding\r\n\r\n">>, State=#state_rcv{session=#http{user_agent="Firefox"}}, {Rep, [], false } =ts_http:parse(Res,State), ?assertMatch(#http{user_agent="Firefox",status={none,304}, partial=false}, Rep#state_rcv.session). split_body_test() -> Data = << "HTTP header\r\nHeader: value\r\n\r\nbody\r\n" >>, ?assertEqual({<< "HTTP header\r\nHeader: value" >>, << "body\r\n" >>}, ts_http:split_body(Data)). split_body2_test() -> Data = << "HTTP header\r\nHeader: value\r\n\r\nbody\r\n\r\nnewline in body\r\n" >>, ?assertEqual({<< "HTTP header\r\nHeader: value" >>, << "body\r\n\r\nnewline in body\r\n" >>}, ts_http:split_body(Data)). split_body_no_newline_test() -> Data = << "HTTP header\r\nHeader: value\r\n\r\nbody" >>, ?assertEqual({<< "HTTP header\r\nHeader: value" >>, << "body" >>}, ts_http:split_body(Data)). split_body3_test() -> Data = << "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\n19\r\nbody\r\n\r\nnewline in body\r\n\r\n" >>, ?assertEqual({<< "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked" >>, << "19\r\nbody\r\n\r\nnewline in body\r\n\r\n" >>}, ts_http:split_body(Data)). decode_buffer_test() -> Data = << "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\n19\r\nbody\r\n\r\nnewline in body\r\n0\r\n\r\n" >>, ?assertEqual(<< "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\nbody\r\n\r\nnewline in body\r\n" >>, ts_http:decode_buffer(Data, #http{chunk_toread=-2})). decode_buffer2_test() -> Data = << "HTTP header\r\nHeader: value\r\n\r\nbody\r\n\r\nnewline in body\r\n" >>, ?assertEqual(<< "HTTP header\r\nHeader: value\r\n\r\nbody\r\n\r\nnewline in body\r\n" >>, ts_http:decode_buffer(Data, #http{chunk_toread=-1}) ). decode_buffer3_test() -> Data = << "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\n17\r\nbody\r\n\r\nnewline in body\r\n3\r\nabc\r\n0\r\n\r\n" >>, ?assertEqual(<< "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\nbody\r\n\r\nnewline in bodyabc" >>, ts_http:decode_buffer(Data, #http{chunk_toread=-2})). compress_chunk_test()-> <> = zlib:gzip("sesame ouvre toi"), Data1 = << "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\nA\r\n" >>, Data2= <<"1A\r\n" >>, Data3= <<"0\r\n\r\n" >>, Data= <>, ?assertEqual(<< "HTTP header\r\nHeader: value\r\nTransfer-Encoding: chunked\r\n\r\nsesame ouvre toi" >>, ts_http:decode_buffer(Data, #http{chunk_toread=-2, compressed={false,gzip}})). authentication_basic_test()-> Base="QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ?assertEqual(["Authorization: Basic ",Base,?CRLF], ts_http_common:authenticate(#http_request{userid="Aladdin", auth_type="basic",passwd="open sesame"})). authentication_digest1_test()-> OK="Authorization: Digest username=\"Mufasa\", realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", uri=\"/dir/index.html\", response=\"6629fae49393a05397450978507c4ef1\", opaque=\"5ccc069c403ebaf9f0171e9517f40e41\", qop=\"auth\", nc=00000001, cnonce=\"0a4f113b\"\r\n", Req=#http_request{userid="Mufasa", auth_type="digest",passwd="Circle Of Life", realm ="testrealm@host.com", url="/dir/index.html", digest_qop = "auth", digest_nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093", digest_nc = "00000001", digest_cnonce = "0a4f113b", digest_opaque = "5ccc069c403ebaf9f0171e9517f40e41"}, ?assertEqual(OK, lists:flatten(ts_http_common:authenticate(Req))). oauth_test()-> myset_env(), Data = <<"HTTP/1.1 200 OK\r\nDate: Mon, 10 Sep 2012 12:26:35 GMT\r\nServer: Apache/2.2.17 (Debian)\r\nX-Powered-By: PHP/5.3.3-7\r\nContent-Length: 55\r\nContent-Type: text/html\r\n\r\noauth_token=requestkey&oauth_token_secret=requestsecret">>, ?assertMatch([{'token',<< "requestsecret" >>}], ts_search:parse_dynvar([{re,'token', "oauth_token_secret=([^&]*)"} ],Data)), ?assertMatch([{'token',<< "requestkey" >>}], ts_search:parse_dynvar([{re,'token', "oauth_token=([^&]*)"} ],Data)). set_msg_dyn_test() -> URL = "http://jm-11:%%_myport%%/%%_myurl%%", Subst =true, Res = ts_config_http:set_msg(#http_request{url= URL}, {Subst, undefined, false, [#server{host="myserver", port=99, type="tcp"}], "myserver", ets:new(fake,[]), 1}), ?assertMatch(#http_request{url=URL}, Res#ts_request.param). set_msg_test() -> URL = "http://server:8080/path%%bla%%", Subst = false, Res = ts_config_http:set_msg(#http_request{url= URL}, {Subst, undefined, false, [#server{host="myserver", port=99, type="tcp"}], "myserver", ets:new(fake,[]), 1}), ?assertMatch(#http_request{url="/path%%bla%%",host_header= "server:8080"}, Res#ts_request.param), ?assertMatch(#ts_request{host="server", port=8080, scheme = ts_tcp}, Res). set_msg2_test() -> URL = "http://server:8080/path%%", Subst = true, Res = ts_config_http:set_msg(#http_request{url= URL}, {Subst, undefined, false, [#server{host="myserver", port=99, type="tcp"}], "myserver", ets:new(fake,[]), 1}), ?assertMatch(#http_request{url="/path%%",host_header= "server:8080"}, Res#ts_request.param), ?assertMatch(#ts_request{host="server", port=8080, scheme = ts_tcp}, Res). myset_env()-> myset_env(0). myset_env(N)-> application:set_env(stdlib,debug_level,N). tsung-1.8.0/src/test/ts_test_file_server.erl0000644000201100017670000000672414377756736020733 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_recorder.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 20 Mar 2005 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_file_server). -compile(export_all). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include_lib("eunit/include/eunit.hrl"). -define(CSVSIZE,10000). test()-> ok. config_file_server1_test()-> myset_env(), ts_file_server:start(), ts_file_server:read([{default,"./src/test/test_file_server.csv"}, {user,"./src/test/test_file_server2.csv"} ]), ?assertMatch({ok,<< "username1;glop;">> }, ts_file_server:get_next_line()). config_file_server2_test()-> myset_env(), ?assertMatch({ok,<< "username2;;" >> }, ts_file_server:get_next_line()). config_file_server3_test()-> myset_env(), ?assertMatch({ok,<< "user1;sesame">> }, ts_file_server:get_next_line(user)). config_file_server4_test()-> myset_env(), ?assertMatch({ok,<< "username3;glop4;">> }, ts_file_server:get_next_line()). config_file_server_dynfun_test()-> myset_env(), ?assertMatch( << "username1;glop;">>, ts_file_server:get_next_line({self(), {}})). config_file_server_huge_test()-> myset_env(), ts_file_server:stop(), ts_file_server:start(), CSV=lists:foldl(fun(I,Acc)-> IStr=integer_to_list(I), [Acc,"user",IStr,";passwd",IStr,"\n"] end, [],lists:seq(1,?CSVSIZE)), File="./src/test/usersdb.csv", file:write_file(File,list_to_binary(CSV)), {Time1, Out } = timer:tc(ts_file_server, read, [[{default,File}]]), erlang:display([?CSVSIZE," read_file:", Time1]), ?assertMatch(ok, Out). config_file_server_huge_get_random_test()-> {Time2, Out } = timer:tc( lists, foreach, [ fun(_)-> ts_file_server:get_random_line() end,lists:seq(1,?CSVSIZE)]), erlang:display([?CSVSIZE," get all lines (random):", Time2]), ?assertMatch(ok, Out ). config_file_server_huge_get_next_test()-> {Time2, Out } = timer:tc( lists, foreach, [ fun(_)-> ts_file_server:get_next_line() end,lists:seq(1,?CSVSIZE)]), erlang:display([?CSVSIZE," get all lines:", Time2]), ?assertMatch(ok, Out ). config_file_server_cycle_test()-> myset_env(), ts_file_server:stop(), ts_file_server:start(), ts_file_server:read([{default,"./src/test/test_file_server.csv"}]), ts_file_server:get_next_line(), ts_file_server:get_next_line(), ts_file_server:get_next_line(), ?assertMatch({ok,<< "username1;glop;">> }, ts_file_server:get_next_line()). config_file_server_all_test()-> myset_env(), ?assertMatch({ok,[<< "username1;glop;">> ,<< "username2;;">> ,<< "username3;glop4;">> ]}, ts_file_server:get_all_lines()). file_to_list_test()-> Val = [ "username1;glop;", "username2;;" , "username3;glop4;" ], ?assertMatch({ok, Val},ts_utils:file_to_list("./src/test/test_file_server.csv")). split_test()-> ?assertEqual([<<"username3" >>, <<"glop4">>, <<>>], ts_utils:split(<< "username3;glop4;">>, <<";">>)). split2_test()-> ?assertEqual([<< >>], ts_utils:split(<< "">>, <<";">>)). myset_env()-> myset_env(0). myset_env(V)-> application:set_env(stdlib,file_server_timeout,30000), application:set_env(stdlib,debug_level,V), application:set_env(stdlib,thinktime_override,"false"), application:set_env(stdlib,thinktime_random,"false"). tsung-1.8.0/src/test/ts_test_dynvars_api.erl0000644000201100017670000000555614377756736020747 0ustar nniclausdream%% ts_test_dynvars_api.erl %% @author Pablo Polvorin %% @doc Test for the ts_dynvars module %% created on 2008-08-22 -module(ts_test_dynvars_api). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). from_keyval_list(KeyValues) -> lists:foldl(fun({K,V},DynVars) -> ts_dynvars:set(K,V,DynVars) end, ts_dynvars:new(), KeyValues ). test() -> ok. dynvars_new_ok_test() -> Keys = [one,two,three,four], Values = [1,2,"three",4], ?assertEqual([{one,1},{two,2},{three,"three"},{four,4}], ts_dynvars:new(Keys, Values)). dynvars_array_test() -> Keys = [one,two,three,four], Values = [[10,11,12],2,"three",4], DynVars= ts_dynvars:new(Keys, Values), ?assertEqual({ok,[10,11,12]}, ts_dynvars:lookup(one, DynVars)), ?assertEqual({ok,11}, ts_dynvars:lookup({one,2}, DynVars)). dynvars_new_more_test() -> Keys = [one,two,three], Values = [1,2,"three",[]], ?assertEqual([{one,1},{two,2},{three,"three"}], ts_dynvars:new(Keys, Values)). dynvars_new_less_test() -> Keys = [one,two,three,four], Values = [1,2,"three"], ?assertEqual([{one,1},{two,2},{three,"three"},{four,""}], ts_dynvars:new(Keys, Values)). dynvars_set_test() -> KeyValues = [one,two,three,four], DynVars = from_keyval_list([{K,K} || K <- KeyValues]), ?assertEqual([{ok,K} || K <- KeyValues], [ts_dynvars:lookup(Key, DynVars) || Key <- KeyValues]). dynvars_set2_test() -> D = ts_dynvars:set(one,two, ts_dynvars:set(one,one, ts_dynvars:new())), ?assertEqual({ok,two},ts_dynvars:lookup(one,D)). dynvars_undefined_test() -> ?assertEqual(false,ts_dynvars:lookup(one, ts_dynvars:new())). dynvars_default_test() -> ?assertEqual({ok,default},ts_dynvars:lookup(one,ts_dynvars:new(), default)). dynvars_entries_test() -> KeyValues = [{K,K} || K <- [one,two,three,four]], ?assertEqual(lists:reverse(KeyValues), ts_dynvars:entries(from_keyval_list(KeyValues))). dynvars_map_test() -> KeyValues = [{K,K} || K <- [one,two,three,four]], ?assertEqual({ok,[two,two]},ts_dynvars:lookup(two,ts_dynvars:map(fun(X) -> [X,X] end, two, default, from_keyval_list(KeyValues)) )). dynvars_map_default_test() -> KeyValues = [{K,K} || K <- [one,two,three,four]], ?assertEqual({ok,[one]},ts_dynvars:lookup(five, ts_dynvars:map(fun(X) -> [one|X] end, five, [], from_keyval_list(KeyValues)) )). tsung-1.8.0/src/test/ts_test_config.erl0000644000201100017670000002740114377756736017666 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_recorder.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 20 Mar 2005 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_config). -compile(export_all). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include_lib("eunit/include/eunit.hrl"). -include("xmerl.hrl"). -include("ts_http.hrl"). test()-> ok. popularity_test() -> ?assertError({"can't mix probabilities and weights",10,10}, ts_config:get_popularity(10,10,undefined,100)), ?assertError({"can't use probability when using weight"}, ts_config:get_popularity(10,-1,true,100)), ?assertError({"can't use weights when using probabilities"}, ts_config:get_popularity(-1,10,false,100)), ?assertEqual({10,false,110}, ts_config:get_popularity(10,-1,false,100)), ?assertEqual({10,true,110}, ts_config:get_popularity(-1,10,true,100)), ?assertEqual({30,false,60}, ts_config:get_popularity(30,-1,false,30)), ?assertError({"must set weight or probability in session"} , ts_config:get_popularity(-1,-1,undefined,100)), ?assertError({"can't mix probabilities and weights",0,0}, ts_config:get_popularity(0,0,true,100)), ?assertError({"can't mix probabilities and weights",0,0}, ts_config:get_popularity(0,0,false,100)), ?assertEqual({0,true,100}, ts_config:get_popularity(-1,0,true,100)), ?assertEqual({0,false,100}, ts_config:get_popularity(0,-1,false,100)). read_config_http_test() -> myset_env(), ?assertMatch({ok, Config}, ts_config:read("./examples/http_simple.xml",".")). read_config_http2_test() -> myset_env(), ?assertMatch({ok, Config}, ts_config:read("./examples/http_distributed.xml",".")). read_config_pgsql_test() -> myset_env(), ?assertMatch({ok, Config}, ts_config:read("./examples/pgsql.xml",".")). read_config_jabber_test() -> myset_env(), ts_user_server:start([]), ?assertMatch({ok, Config}, ts_config:read("./examples/jabber.xml",".")). read_config_jabber_muc_test() -> myset_env(), ts_user_server:start([]), ?assertMatch({ok, Config}, ts_config:read("./examples/jabber_muc.xml",".")). read_config_xmpp_muc_test() -> myset_env(), ts_user_server:start([]), ?assertMatch({ok, Config}, ts_config:read("./src/test/xmpp-muc.xml",".")). config_get_session_test() -> myset_env(0), ts_user_server:start([]), ts_config_server:start_link(["/tmp"]), ok = ts_config_server:read_config("./examples/http_setdynvars.xml"), {ok, Session=#session{userid=1,dump=full} } = ts_config_server:get_next_session({"localhost",1}), ?assertEqual(1, Session#session.id). config_get_session_size_test() -> myset_env(), {ok, Session=#session{userid=2} } = ts_config_server:get_next_session({"localhost",1}), ?assertEqual(13, Session#session.size). read_config_badpop_test() -> myset_env(), ts_user_server:start([]), {ok, Config} = ts_config:read("./src/test/badpop.xml","."), ?assertMatch({error,{bad_sum,_,_}}, ts_config_server:check_config( ts_config_server:compute_popularities(Config))). read_config_thinkfirst_test() -> myset_env(), ?assertMatch({ok, Config}, ts_config:read("./src/test/thinkfirst.xml",".")). config_minmax_test() -> myset_env(), {ok, Session=#session{userid=3} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, ?assertMatch({thinktime,{range,2000,4000}}, ts_config_server:get_req(Id,7)). config_minmax2_test() -> myset_env(), {ok, Session=#session{userid=4} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, {thinktime, Req} = ts_config_server:get_req(Id,7), Think=ts_client:set_thinktime(Req), Resp = receive Data-> Data end, ?assertMatch({timeout,_,end_thinktime}, Resp). config_thinktime_test() -> myset_env(), ok = ts_config_server:read_config("./examples/thinks.xml"), {ok, Session=#session{userid=5} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, {thinktime, Req=2000} = ts_config_server:get_req(Id,5), {thinktime, 2000} = ts_config_server:get_req(Id,7), Think=ts_client:set_thinktime(Req), Resp = receive Data-> Data end, ?assertMatch({timeout,_,end_thinktime}, Resp). config_thinktime2_test() -> myset_env(), ok = ts_config_server:read_config("./examples/thinks2.xml"), {ok, Session=#session{userid=6} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, {thinktime, Req} = ts_config_server:get_req(Id,5), Ref=ts_client:set_thinktime(Req), receive {timeout,Ref2,end_thinktime} -> ok end, random:seed(), % reinit seed for others tests ?assertMatch({random,1000}, Req). read_config_tag_noexclusion_test() -> %% no exclusion all request will be played myset_env(), ok = ts_config_server:read_config("./examples/http_tag.xml"), {ok, Session=#session{userid=7} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, ReqRef = #http_request{url="/img/excluded.png"}, {ts_request,parse,false,[],[],Req,_,_,_,_} = ts_config_server:get_req(Id,2), ?assertEqual(ReqRef#http_request.url, Req#http_request.url). read_config_tag_one_test() -> %% one tag defined %% exclude urls tagged as 'landing' myset_env(), application:set_env(stdlib,exclude_tag,"landing"), ok = ts_config_server:read_config("./examples/http_tag.xml"), {ok, Session=#session{userid=8} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, ReqRef = #http_request{url="/img/excluded.gif"}, {ts_request,parse,false,[],[],Req,_,_,_,_} = ts_config_server:get_req(Id,2), ?assertEqual(ReqRef#http_request.url, Req#http_request.url). read_config_tag_two_test() -> %% two tag defined %% exclude urls tagged as 'landing' and 'gif' myset_env(), application:set_env(stdlib,exclude_tag,"gif,landing"), ok = ts_config_server:read_config("./examples/http_tag.xml"), {ok, Session=#session{userid=9} } = ts_config_server:get_next_session({"localhost",1}), Id = Session#session.id, ReqRef = #http_request{url="/img/not-excluded.png"}, {ts_request,parse,false,[],[],Req,_,_,_,_} = ts_config_server:get_req(Id,2), ?assertEqual(ReqRef#http_request.url, Req#http_request.url). config_arrivalrate_test() -> myset_env(), ok = ts_config_server:read_config("./examples/thinks.xml"), {ok, {[Phase1,Phase2, Phase3],_,_} } = ts_config_server:get_client_config("localhost"), RealDur = 10 * 60 * 1000, RealNU = 1200, RealIntensity = 2 / 1000, ?assertEqual(#phase{intensity=RealIntensity,nusers = RealNU,duration = RealDur}, Phase1), ?assertEqual(#phase{intensity=RealIntensity/60, nusers = RealNU div 60, duration = RealDur}, Phase2), ?assertEqual(#phase{intensity=RealIntensity/3600,nusers = 12, duration = RealDur*36}, Phase3). config_interarrival_test() -> myset_env(), ok = ts_config_server:read_config("./examples/thinks2.xml"), {ok, {[Phase1,Phase2, Phase3],_,_} } = ts_config_server:get_client_config("localhost"), RealDur = 10 * 60 * 1000, RealNU = 1200, RealIntensity = 2 / 1000, ?assertEqual(#phase{intensity=RealIntensity, nusers = RealNU, duration=RealDur}, Phase1), ?assertEqual(#phase{intensity=RealIntensity/60, nusers = RealNU div 60, duration = RealDur}, Phase2), ?assertEqual(#phase{intensity=RealIntensity/3600, nusers = 12, duration = RealDur*36}, Phase3). read_config_maxusers_test() -> read_config_maxusers({5,15},10,"./src/test/thinkfirst.xml"). read_config_maxusers({MaxNumber1,MaxNumber2},Clients,File) -> myset_env(), C=lists:map(fun(A)->"client"++integer_to_list(A) end, lists:seq(1,Clients)), ts_config_server:read_config("./src/test/thinkfirst.xml"), {M1,M2} = lists:unzip(lists:map(fun(X)-> {ok,{[#phase{nusers = Max},#phase{nusers = Max2} ],_,_}} = ts_config_server:get_client_config(X), {Max,Max2} end, C)), [Head1|_]=M1, [Head2|_]=M2, ?assertEqual(1, Head1), ?assertEqual(1, Head2), ?assert(lists:min(M1) >= 0), ?assert(lists:min(M2) >= 0), ?assertEqual(lists:sum(M1), MaxNumber1), ?assertEqual(lists:sum(M2), MaxNumber2). read_config_static_test() -> myset_env(), C=lists:map(fun(A)->"client"++integer_to_list(A) end, lists:seq(1,10)), M = lists:map(fun(X)-> {ok,Res,_} = ts_config_server:get_client_config(static,X), ?LOGF("X: ~p~n",[length(Res)],?ERR), length(Res) end, C), ?assertEqual(lists:sum(M) , 5). cport_list_node_test() -> List=['tsung1@toto', 'tsung3@titi', 'tsung2@toto', 'tsung7@titi', 'tsung6@toto', 'tsung4@tutu'], Rep = ts_config_server:get_one_node_per_host(List), ?assertEqual(['tsung1@toto', 'tsung3@titi', 'tsung4@tutu'], lists:sort(Rep)). ifalias_test() -> Res=ts_ip_scan:get_intf_aliases("lo"), ?assertEqual([{127,0,0,1}],Res). ifalias2_test() -> {ok, L}=ts_utils:file_to_list("src/test/ifcfg.out"), Out=ts_ip_scan:get_intf_aliases(L,"eth0",[],[]), Res=lists:foldl(fun(A,L) -> [{192,168,76,A}|L] end, [],lists:seq(183,190)), ?assertEqual(Out,Res). ifalias_ip_test() -> {ok, L}=ts_utils:file_to_list("src/test/ipcfg.out"), Out=ts_ip_scan:get_ip_aliases(L,[]), Res=lists:foldl(fun(A,L) -> [{192,12,0,A}|L] end, [],lists:seq(1,12)), ?assertEqual(Out,Res). encode_test() -> Encoded="ts_encoded_47myfilepath_47toto_47titi_58sdfsdf_45sdfsdf_44aa_47", Str="/myfilepath/toto/titi:sdfsdf-sdfsdf,aa/", ?assertEqual(Encoded,ts_config_server:encode_filename(Str)). decode_test() -> Encoded="ts_encoded_47myfilepath_47toto_47titi_58sdfsdf_45sdfsdf_44aa_47", Str="/myfilepath/toto/titi:sdfsdf-sdfsdf,aa/", ?assertEqual(Str,ts_config_server:decode_filename(Encoded)). concat_atoms_test() -> ?assertEqual('helloworld', ts_utils:concat_atoms(['hello','world'])). int_or_string_test() -> ?assertEqual(123, ts_config:getAttr(integer_or_string,[#xmlAttribute{name=to,value="123"}],to)). int_or_string2_test() -> ?assertEqual("%%_toto%%", ts_config:getAttr(integer_or_string,[#xmlAttribute{name=to,value="%%_toto%%"}],to)). int_test() -> ?assertEqual(100, ts_config:getAttr(integer,[#xmlAttribute{name=to,value="100"}],to)). launcher_empty_test() -> Intensity=10, Users=2, Duration=25, Phase=#phase{intensity=0,nusers=Users,duration=300, id=1}, NextPhase=#phase{intensity=Intensity,nusers=Users,duration=Duration, id=2}, Res=ts_launcher:wait_static({static,0},#launcher{nusers=0,phases=[NextPhase],current_phase=Phase}), ?LOGF("~p",[Res],?WARN), ?assertMatch({next_state,launcher,#launcher{phases = [], nusers = Users, current_phase = #phase{nusers=Users,duration=Duration,intensity=Intensity}},_},Res). wildcard_test() -> Names = ["foo1", "foo2", "bar", "barfoo", "foobar", "foo", "fof","glop"], ?assertEqual(["foo1", "foo2", "foobar", "foo"], ts_utils:wildcard("foo*",Names)), ?assertEqual(["foo1", "foo2"], ts_utils:wildcard("foo?",Names)), ?assertEqual(["foobar"], ts_utils:wildcard("foo*r",Names)). myset_env()-> myset_env(0). myset_env(Level)-> catch ts_user_server_sup:start_link() , application:set_env(stdlib,debug_level,Level), application:set_env(stdlib,warm_time,1000), application:set_env(stdlib,thinktime_value,"5"), application:set_env(stdlib,thinktime_override,"false"), application:set_env(stdlib,thinktime_random,"false"), application:set_env(stdlib,exclude_tag,""). tsung-1.8.0/src/test/ts_test_client.erl0000644000201100017670000001121114377756736017667 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_client.erl %%% Author : Rodolphe Quiédeville %%% Description : %%% %%% Created : 7 Oct 2013 by Rodolphe Quiédeville %%%------------------------------------------------------------------- -module(ts_test_client). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). test()-> ok. eq_test() -> ?assertEqual(false, ts_client:rel('eq',5,4)), ?assertEqual(true, ts_client:rel('eq',4,4)). eq_float_test() -> ?assertEqual(false, ts_client:rel('eq',5.0,4.0)), ?assertEqual(true, ts_client:rel('eq',4.0,4.0)). eq_mix_test() -> ?assertEqual(false, ts_client:rel('eq',5.0,4.0)), ?assertEqual(true, ts_client:rel('eq',4.0,4.0)). eq_alist_test() -> ?assertEqual(false, ts_client:rel('eq',"5",4)), ?assertEqual(true, ts_client:rel('eq',"4",4)). eq_blist_test() -> ?assertEqual(false, ts_client:rel('eq',<<"5">>,"4")), ?assertEqual(true, ts_client:rel('eq',<<"4">>,"4")). eq_binary_test() -> ?assertEqual(false, ts_client:rel('eq',<<"5">>,"4")), ?assertEqual(true, ts_client:rel('eq',<<"4">>,"4")), ?assertEqual(false, ts_client:rel('eq',"5",<<"4">>)), ?assertEqual(true, ts_client:rel('eq',"4",<<"4">>)). eq_aatom_test() -> ?assertEqual(false, ts_client:rel('eq', foo, <<"foobar">>)), ?assertEqual(true, ts_client:rel('eq', foo, <<"foo">>)). eq_batom_test() -> ?assertEqual(false, ts_client:rel('eq',<<"barfoo">>, foobar)), ?assertEqual(true, ts_client:rel('eq',<<"foobar">>, foobar)). neq_int_test() -> ?assertEqual(true, ts_client:rel('neq',5,4)), ?assertEqual(false, ts_client:rel('neq',4,4)). neq_list_test() -> ?assertEqual(true, ts_client:rel('neq',"e","4")), ?assertEqual(false, ts_client:rel('neq',"4","4")). neq_binary_test() -> ?assertEqual(true, ts_client:rel('neq',<<"ed">>,<<"4">>)), ?assertEqual(false, ts_client:rel('neq',<<"4">>,<<"4">>)). need_jump_while_test()-> ?assertEqual(true, ts_client:need_jump('while',true)), ?assertEqual(false, ts_client:need_jump('while',false)). need_jump_until_test()-> ?assertEqual(false, ts_client:need_jump('until',true)), ?assertEqual(true, ts_client:need_jump('until',false)). need_jump_if_test()-> ?assertEqual(false, ts_client:need_jump('if',true)), ?assertEqual(true, ts_client:need_jump('if',false)). binary_to_num_int_test()-> ?assertEqual(100, ts_client:binary_to_num(<<"100">>)). binary_to_num_float_test()-> ?assertEqual(100.1, ts_client:binary_to_num(<<"100.1">>)). binary_to_num_float_neg_test()-> ?assertEqual(-3.14, ts_client:binary_to_num(<<"-3.14">>)). gt_int_test()-> ?assertEqual(true, ts_client:rel('gt',<<"2">>,<<"3">>)), ?assertEqual(true, ts_client:rel('gt',<<"-2">>,<<"-1">>)), ?assertEqual(false, ts_client:rel('gt',<<"2">>,<<"2">>)), ?assertEqual(false, ts_client:rel('gt',<<"22">>,<<"3">>)). gt_float_test()-> ?assertEqual(true, ts_client:rel('gt',<<"2.0">>,<<"3.1">>)), ?assertEqual(false, ts_client:rel('gt',<<"2.0">>,<<"2.0">>)), ?assertEqual(false, ts_client:rel('gt',<<"22.1">>,<<"3.0">>)). lt_int_test()-> ?assertEqual(false, ts_client:rel('lt',<<"2">>,<<"3">>)), ?assertEqual(false, ts_client:rel('lt',<<"2">>,<<"2">>)), ?assertEqual(true, ts_client:rel('lt',<<"22">>,<<"3">>)). lt_float_test()-> ?assertEqual(false, ts_client:rel('lt',<<"2.0">>,<<"3.1">>)), ?assertEqual(false, ts_client:rel('lt',<<"2.0">>,<<"2.0">>)), ?assertEqual(true, ts_client:rel('lt',<<"22.1">>,<<"3.0">>)). gte_int_test()-> ?assertEqual(true, ts_client:rel('gte',<<"2">>,<<"3">>)), ?assertEqual(true, ts_client:rel('gte',<<"2">>,<<"2">>)), ?assertEqual(false, ts_client:rel('gte',<<"22">>,<<"3">>)). gte_float_test()-> ?assertEqual(true, ts_client:rel('gte',<<"2.0">>,<<"3.1">>)), ?assertEqual(true, ts_client:rel('gte',<<"-2.0">>,<<"-1.31">>)), ?assertEqual(true, ts_client:rel('gte',<<"2.0">>,<<"2.0">>)), ?assertEqual(false, ts_client:rel('gte',<<"22.1">>,<<"3.0">>)). lte_int_test()-> ?assertEqual(false, ts_client:rel('lte',<<"2">>,<<"3">>)), ?assertEqual(true, ts_client:rel('lte',<<"-2">>,<<"-3">>)), ?assertEqual(true, ts_client:rel('lte',<<"2">>,<<"2">>)), ?assertEqual(true, ts_client:rel('lte',<<"22">>,<<"3">>)). lte_float_test()-> ?assertEqual(false, ts_client:rel('lte',<<"2.0">>,<<"3.1">>)), ?assertEqual(true, ts_client:rel('lte',<<"-2.0">>,<<"-3.1">>)), ?assertEqual(true, ts_client:rel('lte',<<"2.0">>,<<"2.0">>)), ?assertEqual(true, ts_client:rel('lte',<<"-2.0">>,<<"-2.0">>)), ?assertEqual(true, ts_client:rel('lte',<<"22.1">>,<<"3.0">>)). tsung-1.8.0/src/test/ts_test_all.erl0000644000201100017670000001445514377756736017176 0ustar nniclausdream%%%------------------------------------------------------------------- %%% File : ts_test_all.erl %%% Author : Nicolas Niclausse %%% Description : run all test functions %%% %%% Created : 17 Mar 2007 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_test_all). -compile(export_all). -include_lib("eunit/include/eunit.hrl"). -define(FILE_NAME(MODULE), "cover_report/" ++ atom_to_list(MODULE) ++ ".html"). version() -> case re:run(erlang:system_info(otp_release), "R?(\\d+)B?-?(\\d+)?", [{capture, all, list}]) of {match, [_Full, Maj, Min]} -> {list_to_integer(Maj), list_to_integer(Min)}; {match, [_Full, Maj]} -> {list_to_integer(Maj), 0} end. run() -> case version() of {Maj, Min} when (Maj > 16 orelse ((Maj == 16) andalso (Min >= 3))) -> run_cover(); _ -> %% older version of cover removes the export_all option RetVal = case eunit:test([ts_test_all], [{report,{eunit_surefire,[{dir,"."}]}}]) of error -> 1; _ -> 0 end, init:stop(RetVal) end. run_cover() -> {ok, Path} = file:get_cwd(), Dir = filename:join(Path,"ebin-test"), cover:compile_beam_directory(Dir), ModulesAll = cover:modules(), Modules = lists:filter(fun(M) -> case atom_to_list(M) of "ts_test" ++ _ -> false; "ts_" ++ _ -> true; "tsung" ++ _ -> true; _ -> false end end , ModulesAll), filelib:ensure_dir("cover_report/index.html"), RetVal = case eunit:test([ts_test_all], [{report,{eunit_surefire,[{dir,"."}]}}]) of error -> 1; Result -> 0 end, lists:foreach(fun(M) -> cover:analyse_to_file(M, ?FILE_NAME(M), [html]) end, Modules), write_report(lists:sort(Modules)), init:stop(RetVal). test() -> ok. all_test_() -> [ts_test_recorder, ts_test_config, ts_test_client, ts_test_dynvars_api, ts_test_file_server, ts_test_options, ts_test_pgsql, ts_test_proxy, ts_test_http, ts_test_jabber, ts_test_match, ts_test_mochi, ts_test_mon, ts_test_user_server, ts_test_search, ts_test_stats, ts_test_interaction, ts_test_websocket, ts_test_utils, ts_test_mqtt ]. %%% The two following functions are copyrighted by: %%% ---------------------------------------------------------------------------- %%% Copyright (c) 2009, Erlang Training and Consulting Ltd. %%% All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% * Redistributions of source code must retain the above copyright %%% notice, this list of conditions and the following disclaimer. %%% * Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% * Neither the name of Erlang Training and Consulting Ltd. nor the %%% names of its contributors may be used to endorse or promote products %%% derived from this software without specific prior written permission. %%% %%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS'' %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE %%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE %%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR %%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR %%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF %%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %%% ---------------------------------------------------------------------------- %%% @author Oscar Hellstrom write_report(Modules) -> {TotalPercentage, ModulesPersentage} = percentage(Modules, 0, 0, []), io:format(standard_io," Total test coverage: ~p %~n",[TotalPercentage]), file:write_file("cover_report/index.html", [ "\nCover report index\n" "\n" "

Cover report for Tsung

" "Total coverage: ", integer_to_list(TotalPercentage), "%" "

Cover for individual modules

\n" "
    \n\t", lists:foldl(fun({Module, Percentage}, Acc) -> Name = atom_to_list(Module), [ "
  • " "", Name, " ", integer_to_list(Percentage), "%" "
  • \n\t" | Acc ] end, [], ModulesPersentage), "
" ]). percentage([Module | Modules], TotCovered, TotLines, Percentages) -> {ok, Analasys} = cover:analyse(Module, coverage, line), case lists:foldl(fun({_, {C, _}}, {Covered, Lines}) -> {C + Covered, Lines + 1} end, {0, 0}, Analasys) of {_,0} -> percentage(Modules, TotCovered, TotLines, Percentages); {Covered, Lines} -> Percent = (Covered * 100) div Lines, NewPercentages = [{Module, Percent} | Percentages], percentage(Modules, Covered + TotCovered, Lines + TotLines, NewPercentages) end; percentage([], Covered, Lines, Percentages) -> {(Covered * 100) div Lines, Percentages}. tsung-1.8.0/src/tsung_controller/0000755000201100017670000000000014377757020016553 5ustar nniclausdreamtsung-1.8.0/src/tsung_controller/tsung_controller.app.in0000644000201100017670000000757214377756736023315 0ustar nniclausdream{application, tsung_controller, [{description, "tsung, a bench tool for TCP/UDP servers"}, {vsn, "@PACKAGE_VERSION@"}, {modules, [ ts_api, ts_config_amqp, ts_config, ts_config_fs, ts_config_http, ts_config_jabber, ts_config_job, ts_config_ldap, ts_config_mqtt, ts_config_mysql, ts_config_pgsql, ts_config_raw, ts_config_server, ts_config_shell, ts_config_websocket, ts_controller_sup, ts_file_server, ts_interaction_server, ts_job_notify, ts_match_logger, ts_mon, ts_msg_server, ts_os_mon, ts_os_mon_erlang, ts_os_mon_munin, ts_os_mon_snmp, ts_os_mon_sup, ts_stats_mon, ts_timer, tsung_controller, ts_user_server, ts_user_server_sup, ts_web ]}, {registered, [ ts_config_server, ts_file_server, ts_interaction_server, ts_job_notify, ts_match_logger, ts_mon, ts_msg_server, ts_os_mon, ts_os_mon_erlang, ts_os_mon_snmp, ts_os_mon_munin, ts_stats_mon, ts_stats, ts_timer, ts_user_server ]}, {env, [ {debug_level, 6}, {smp_disable, true}, % disable smp on clients {ts_cookie, "humhum"}, {clients_timeout, 60000}, % timeout for global synchro {file_server_timeout, 30000},% timeout for reading file {warm_time, 1}, % (seconds) initial waiting time when launching clients {thinktime_value, "5"}, % default value = 5sec {thinktime_override, "false"}, {thinktime_random, "false"}, {global_number, 100}, {global_ack_timeout, 3600000}, %in msec {munin_port, 4949}, {snmp_port, 161}, {snmp_version, v2}, {snmp_community, "public"}, {mysql_port, 3306}, {mysql_user, "root"}, {mysql_password, false}, {dumpstats_interval, 10000}, {dump, none}, %% full or light or none {stats_backend, none}, %% text|rrdtool {nclients, 10}, %% number of clients {nclients_deb, 1}, %% beginning of interval {nclients_fin, 2000}, %% end of interval {config_file, "./tsung.xml"}, {log_file, "./tsung.log"}, {match_log_file, "./match.log"}, {exclude_tag, ""}, {template_path, beam_relative} ]}, {applications, [ @ERLANG_APPLICATIONS@ ]}, {start_phases, [{load_config, []},{start_os_monitoring,[{timeout,30000}]}, {start_clients,[]}]}, {mod, {tsung_controller, []}} ]}. tsung-1.8.0/src/tsung_controller/ts_web.erl0000644000201100017670000003614314377756736020566 0ustar nniclausdream%%% %%% Copyright 2014 Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 23 avril 2014 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% -module(ts_web). -vc('$Id: ts_web.erl,v 0.0 2014/04/23 12:12:17 nniclaus Exp $ '). -author('nicolas@niclux.org'). -include("ts_macros.hrl"). -include_lib("kernel/include/file.hrl"). -export([start/0, status/3, stop/3, logs/3, update/3, graph/3, error/3, report/3]). -export([number_to_list/1]). start() -> error_logger:tty(false), Redirect= << "\n" >>, ts_controller_sup:start_inets(?config(log_dir), Redirect). graph(SessionID, Env, Input) -> graph(SessionID, Env, Input,"graph.html"). report(SessionID, Env, Input) -> graph(SessionID, Env, Input,"report.html"). graph(SessionID, Env, Input, File) -> Begin=?NOW, {ok,Path} = application:get_env(tsung_controller,log_dir_real), GraphFile = filename:join(Path,File), case update_reports() of {error, not_found} -> Msg = " Fail to generated reports: tsung_stats.pl was not found in the $PATH or in:
" ++script_paths(), error(SessionID, Env, Input, Msg); _ -> case file:read_file(GraphFile) of {error, enoent} -> update(SessionID, Env, Input); {ok, Data} -> Time=ts_utils:elapsed(Begin,?NOW), Text="
"++ ts_utils:datestr() ++ ": Report and graphs generated in "++ number_to_list(Time/1000) ++" sec
", WorkingDir=filename:basename(Path), Str=replace(Data,[{"=\"style/","=\"/style/"}, {"\"graph.html","\"/es/ts_web:graph"}, {"\"report.html","\"/es/ts_web:report"}, {"csv_data","/csv_data"}, {"",Text}, {"","Dashboard - " ++ WorkingDir} ]), mod_esi:deliver(SessionID, [ "Content-Type: text/html\r\n\r\n", Str ]) end end. error(SessionID, Env, Input) -> error(SessionID, Env, Input, ""). error(SessionID, _Env, _Input, Msg) -> Title = "Tsung Update Error", Text = "
"++ Msg ++"
", mod_esi:deliver(SessionID,["Content-Type: text/html\r\n\r\n", head(Title) ++ " " ++ nav() ++ sidebar() ++ "
" ++ Text ++ foot() ] ). script_paths()-> {ok,Path} = application:get_env(tsung_controller,log_dir_real), UserPath = filename:join(Path,"../../../lib/tsung/bin"), ts_utils:join(":",[UserPath,"/usr/lib64/tsung/bin/","/usr/lib/tsung/bin","/usr/local/lib/tsung/bin"]). update_reports() -> %% Referer = proplists:get_value(http_referer,Env), {ok,Path} = application:get_env(tsung_controller,log_dir_real), case os:find_executable("tsung_stats.pl") of false -> case os:find_executable("tsung_stats.pl", script_paths()) of false -> {error, not_found}; RealFile -> Cmd ="cd "++ Path ++ " ; "++ RealFile ++ " --dygraph", os:cmd(Cmd) end; File -> Cmd ="cd "++ Path ++ "; "++ File ++ " --dygraph", os:cmd(Cmd) end. update(SessionID, _Env, _Input) -> Begin=?NOW, Title ="Tsung Update stats", update_reports(), Time=ts_utils:elapsed(Begin,?NOW), mod_esi:deliver(SessionID, [ "Content-Type: text/html\r\n\r\n", head(Title) ++ " " ++ nav() ++ sidebar() ++"
" ++ "
Time to update reports:"++ number_to_list(Time/1000) ++" sec
" ++ foot() ]). stop(SessionID, _Env, _Input) -> Title ="Tsung Stop", mod_esi:deliver(SessionID, [ "Content-Type: text/html\r\n\r\n", head(Title) ++ " " ++ "

Tsung controller is stopping now !

" ]), slave:stop(node()). status(SessionID, _Env, _Input) -> Title ="Tsung Status", {ok, Nodes, Ended_Beams, MaxPhases} = ts_config_server:status(), Active = Nodes - Ended_Beams, ActiveBeamsBar = progress_bar(Active,Nodes,"", "Active nodes: "), {Clients, ReqRate, Connected, Interval, Phase, Cpu} = ts_mon:status(), NPhase = case Phase of error -> 1; {ok,N} -> (N div Nodes) + 1 end, RequestsBar = progress_bar(ReqRate/Interval, ReqRate/Interval,"req/sec", lists:flatten("Request rate: ")), PhasesBar = progress_bar(NPhase, MaxPhases,"", lists:flatten("Current phase (total is " ++ number_to_list(MaxPhases) ++" )")), UsersBar = progress_bar(Clients, Clients,"", "Running users"), ConnectedBar = progress_bar(Connected, Clients,"", "Connected users"), CPUBar = progress_bar(Cpu, 100,"", "Controller CPU usage", true), mod_esi:deliver(SessionID, [ "Content-Type: text/html\r\n\r\n", head(Title) ++ " " ++ nav() ++ sidebar() ++ "
" ++ "

Status

" ++ UsersBar ++ ConnectedBar ++ RequestsBar ++ ActiveBeamsBar ++ PhasesBar ++ CPUBar ++ foot() ]). logs(SessionID, _Env, _Input) -> Title ="Tsung Logs", RealPath = case application:get_env(tsung_controller,log_dir_real) of {ok,Path} -> Path; _ -> ?config(log_dir) end, {ok,Files} = file:list_dir(RealPath), FilesHTML = lists:map(fun(F)->format(RealPath,F,"") end,Files), mod_esi:deliver(SessionID, [ "Content-Type: text/html\r\n\r\n", head(Title) ++ "" ++ nav() ++ sidebar() ++ "
" ++ "
"++ FilesHTML ++"
" ++ foot() ]). foot() -> VSN = case lists:keysearch(tsung_controller,1,application:loaded_applications()) of {value, {_,_ ,V}} -> V; _ -> "unknown" end, "
". sidebar() -> "
". head(Title) -> " "++Title ++" ". nav() -> Path = case application:get_env(tsung_controller,log_dir_real) of {ok,P} -> P; _ -> ?config(log_dir) end, WorkingDir=filename:basename(Path), Subtitle = "Dashboard - " ++ WorkingDir, " ". format(Path,Entry,RequestURI) -> case file:read_file_info(filename:join(Path,Entry)) of {ok,FileInfo} when FileInfo#file_info.type == directory -> {{Year, Month, Day},{Hour, Minute, _Second}} = FileInfo#file_info.mtime, EntryLength=length(Entry), if EntryLength > 21 -> io_lib:format("~-21.s.." "~2.2.0w-~s-~w ~2.2.0w:~2.2.0w" " -\n", [RequestURI++"/"++Entry++"/", Entry, Day, httpd_util:month(Month), Year,Hour,Minute]); true -> io_lib:format("~s~*.*c~2.2.0" "w-~s-~w ~2.2.0w:~2.2.0w -\n", [RequestURI ++ "/" ++ Entry ++ "/",Entry, 23-EntryLength,23-EntryLength,$ ,Day, httpd_util:month(Month),Year,Hour,Minute]) end; {ok,FileInfo} -> {{Year, Month, Day},{Hour, Minute,_Second}} = FileInfo#file_info.mtime, EntryLength=length(Entry), if EntryLength > 21 -> io_lib:format(" ~-21.s..~2.2.0" ++ "w-~s-~w ~2.2.0w:~2.2.0w~8wk \n", [RequestURI ++"/"++Entry, Entry,Day, httpd_util:month(Month),Year,Hour,Minute, trunc(FileInfo#file_info.size/1024+1) ]); true -> io_lib:format("~s~*.*c~2.2.0w-~s-~w" ++ " ~2.2.0w:~2.2.0w~8wk \n", [RequestURI ++ "/" ++ Entry, Entry, 23-EntryLength, 23-EntryLength, $ ,Day, httpd_util:month(Month),Year,Hour,Minute, trunc(FileInfo#file_info.size/1024+1) ]) end; {error, _Reason} -> "" end. %% helper functions number_to_list(F) when is_integer(F)-> integer_to_list(F); number_to_list(F) -> io_lib:format("~.2f",[F]). replace(Data,[]) -> binary_to_list(iolist_to_binary(Data)); replace(Data,[{Regexp,Replace}|Tail]) -> replace(re:replace(Data,Regexp,Replace,[global]), Tail). progress_bar(Val, Max, Unit, Title) -> progress_bar(Val, Max, Unit, Title, false). progress_bar(Val, Max, Unit, Title, Variable) -> Percent = case Max of 0 -> 0; 0.0 -> 0; M -> round(100 * Val / M) end, ProgressType = if Variable == false -> "progress-bar-success"; Percent > 80 -> "progress-bar-danger"; Percent > 60 -> "progress-bar-warning"; Percent > 40 -> "progress-bar-info"; true-> "progress-bar-success" end, Title ++ "
" ++ "
" ++ number_to_list(Val) ++ " " ++Unit ++"
". tsung-1.8.0/src/tsung_controller/ts_user_server_sup.erl0000644000201100017670000000365614377756736023247 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% %%% ts_user_server_sup.erl %%% @author Pablo Polvorin %%% @doc %%% created on 2008-09-09 -module(ts_user_server_sup). -export([start_link/0,init/1,start_user_server/1,all_children/0]). -behaviour(supervisor). start_link() -> {ok,Pid} = supervisor:start_link({global,?MODULE},?MODULE,[]), start_default_user_server(), %default user_server is always started {ok,Pid}. init([]) -> SupFlags = {simple_one_for_one,1,1 }, ChildSpec = [ {ts_user_server,{ts_user_server, start, []}, temporary,2000,worker,[ts_user_server]} ], {ok, {SupFlags, ChildSpec}}. start_user_server(Name) -> supervisor:start_child({global,?MODULE},[Name]). start_default_user_server() -> supervisor:start_child({global,?MODULE},[]). all_children() -> [ Pid ||{_,Pid,_,_} <- supervisor:which_children({global,?MODULE})]. tsung-1.8.0/src/tsung_controller/ts_user_server.erl0000644000201100017670000005034314377756736022353 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_user_server). -author('jflecomte@IDEALX.com'). -vc('$Id$ '). -include("ts_macros.hrl"). %%-compile(export_all). -export([reset/1, init_seed/1, get_unique_id/1, get_really_unique_id/1, get_id/0, get_idle/0, get_offline/0, get_online/1, add_to_online/1, remove_from_online/1, remove_connected/1, add_to_connected/1, set_offline_fileid/1, set_random_fileid/1, set_fileid_delimiter/1, get_first/0]). %% for multiple user_server process, one per virtual host -export([reset/2, get_id/1, get_idle/1, get_offline/1, get_online/2, add_to_online/2, remove_from_online/2, remove_connected/2, get_first/1, reset_all/1]). -behaviour(gen_server). %% External exports -export([start/0,start/1, stop/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, { offline, %ets table last_offline, connected, %ets table last_connected, online, %ets table last_online, first_client, % id (integer) random_server_id, % file_server id for random users offline_server_id, % file_server id for initial offline users delimiter = << ";" >>, % delimiter for file_server id username userid_max % max number of ids (starts at 1) }). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start() -> ?LOGF("Starting default user_server ~n",[],?INFO), gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). start(Name) -> ?LOGF("Starting user_server with name ~p ~n",[Name],?INFO), gen_server:start_link({global, Name}, ?MODULE, [], []). reset(default,NFin) -> reset(NFin); reset(UserServer,NFin) -> gen_server:call(UserServer,{reset,NFin}). reset(NFin)-> gen_server:call({global, ?MODULE}, {reset, NFin}). reset_all(NFin) -> lists:foreach(fun(Pid) -> reset(Pid,NFin) end, ts_user_server_sup:all_children()). get_id(default) -> get_id(); get_id(UserServer) -> gen_server:call(UserServer, get_id). get_id()-> gen_server:call({global, ?MODULE }, get_id). %% return a unique id. deprecated since tsung_userid dyn var exists get_unique_id({_, DynVar})-> case ts_dynvars:lookup(tsung_userid,DynVar) of {ok, Val} -> ts_utils:term_to_list(Val); false -> ?LOG("tsung_userid not found ! Can't create unique id~n", ?ERR), "0" end. %% return a really unique id, one that is unique across runs. get_really_unique_id({Pid, DynVars}) -> Sec = ts_utils:now_sec(), ?DebugF("Sec=~p",[Sec]), [[integer_to_list(Sec),"-",get_unique_id({Pid, DynVars})]]. %% get an idle id (offline), and add it to the connected table get_idle(default) -> get_idle(); get_idle(UserServer) -> gen_server:call(UserServer,get_idle). get_idle()-> gen_server:call({global, ?MODULE}, get_idle). %% FIXME: handle vhost add_to_connected(Id)-> gen_server:cast({global, ?MODULE}, {add_to_connected, Id}). get_online(default,Id) -> get_online(Id); get_online(UserServer,Id) when is_list(Id)-> get_online(UserServer,list_to_integer(Id)); get_online(UserServer,Id) -> gen_server:call(UserServer, {get_online, Id}). get_online(Id) when is_list(Id) -> get_online(list_to_integer(Id)); get_online(Id) -> gen_server:call({global, ?MODULE}, {get_online, Id}). %% get an offline id, don't change the connected table. get_offline(default) -> get_offline(); get_offline(UserServer) -> gen_server:call(UserServer, get_offline). get_offline()-> gen_server:call({global, ?MODULE}, get_offline). get_first(default)-> get_first(); get_first(UserServer)-> gen_server:call(UserServer, get_first). get_first()-> gen_server:call({global, ?MODULE}, get_first). remove_connected(default,ID) -> remove_connected(ID); remove_connected(UserServer,Id) when is_list(Id) -> remove_connected(UserServer,list_to_integer(Id)); remove_connected(UserServer,Id) -> gen_server:cast(UserServer, {remove_connected, Id}). remove_connected(Id) when is_list(Id) -> remove_connected(list_to_integer(Id)); remove_connected(Id) -> gen_server:cast({global, ?MODULE}, {remove_connected, Id}). add_to_online(default,Id) -> add_to_online(Id); add_to_online(UserServer,Id) when is_list(Id) -> add_to_online(UserServer,list_to_integer(Id)); add_to_online(UserServer,Id) -> gen_server:cast(UserServer, {add_to_online, Id}). add_to_online(Id) when is_list(Id) -> add_to_online(list_to_integer(Id)); add_to_online(Id) -> gen_server:cast({global, ?MODULE}, {add_to_online, Id}). remove_from_online(default,Id) -> remove_from_online(Id); remove_from_online(UserServer,Id) when is_list(Id) -> remove_from_online(UserServer,list_to_integer(Id)); remove_from_online(UserServer,Id) -> gen_server:cast(UserServer, {remove_from_online, Id}). remove_from_online(Id) when is_list(Id) -> remove_from_online(list_to_integer(Id)); remove_from_online(Id) -> gen_server:cast({global, ?MODULE}, {remove_from_online, Id}). %% @spec set_random_fileid(Id::atom()) -> ok %% @doc Set file_server id for random users %% This is useful and usernames and password are set from a CSV file. %% @end set_random_fileid(Id) -> gen_server:cast({global, ?MODULE}, {set_random_fileid, Id}). %% @spec set_offline_fileid(Id::atom()) -> ok %% @doc Set file_server id for initial offline users %% This is useful and usernames and password are set from a CSV file. %% @end set_offline_fileid(Id) -> gen_server:cast({global, ?MODULE}, {set_offline_fileid, Id}). %% @spec set_fileid_delimiter(D::string()) -> ok %% @doc Set file_server delimiter for random users @end set_fileid_delimiter(D) -> gen_server:cast({global, ?MODULE}, {set_fileid_delimiter, D}). stop()-> lists:foreach(fun(Pid) -> gen_server:call(Pid, stop) end,ts_user_server_sup:all_children()). init_seed(A) -> gen_server:cast({global, ?MODULE}, {init_seed, A}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init(_Args) -> ?LOG("ok, started unconfigured~n", ?INFO), {ok, #state{}}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- %%Get one id in the full list of potential users handle_call(get_id, _From, State=#state{random_server_id = Id, delimiter=D}) when Id /= undefined-> case ts_file_server:get_random_line(Id) of {ok, Line} -> [Val|_] = ts_utils:split(Line,D), {reply, {binary_to_list(Val), unused}, State}; %% FIXME: use binaries in jabber_common everywhere and remove the binary_to_list call here. _Else -> {reply, {error, userid_max_zero }, State} end; handle_call(get_id, _From, State=#state{userid_max = 0}) -> % no user defined in the pool, probably we are using usernames from external file (CSV) {reply, {error, userid_max_zero }, State}; handle_call(get_id, _From, State) -> Key = random:uniform( State#state.userid_max ), {reply, Key, State}; %%Get one id in the users whose have to be connected handle_call(get_idle, _From, State=#state{offline=Offline,connected=Connected}) -> case ets_iterator_next(Offline, State#state.last_offline ) of {error, empty_ets} -> ?LOG("No more free users !~n", ?WARN), {reply, {error, no_free_userid}, State}; {ok, Key} -> NextOffline = ets_iterator_del(Offline, Key, State#state.last_offline), ets:insert(Connected, {Key,1}), case State#state.first_client of undefined -> {reply, Key, State#state{first_client=Key,last_connected=Key, last_offline=NextOffline}}; _Id -> {reply, Key, State#state{last_connected=Key, last_offline=NextOffline}} end end; %%Get one offline id handle_call(get_offline, _From, State=#state{offline=Offline,last_offline=Prev}) -> case ets_iterator_next(Offline, Prev) of {error, _Reason} -> {reply, {error, no_offline}, State}; {ok, {Next,Pwd}} -> ?DebugF("Choose (next is user defined) offline user ~p~n",[Next]), {reply, {ok, {Next,Pwd}}, State#state{last_offline={Next,Pwd}}}; {ok, Next} -> ?DebugF("Choose offline user ~p~n",[Prev]), {reply, {ok, Next}, State#state{last_offline=Next}} end; handle_call(get_first, _From, State) -> {reply, State#state.first_client, State}; handle_call({reset, NFin}, _From, State) -> Offline = ets:new(offline,[ordered_set, private]), Online = ets:new(online, [set, private]), Connected = ets:new(connected, [set, private]), ?LOGF("Reset offline and online lists (maxid=~p)~n",[NFin],?NOTICE), fill_offline(NFin, Offline, {State#state.offline_server_id, State#state.delimiter}), State2 = State#state{offline = Offline, first_client = undefined, last_offline = undefined, connected = Connected, last_connected = undefined, last_online = undefined, online =Online, userid_max=NFin}, {reply, ok, State2}; %%% Get a online id different from 'Id' handle_call( {get_online, Id}, _From, State=#state{ online = Online, last_online = Prev}) -> ?DebugF("get_online from ~p~n",[Id]), case ets_iterator_next(Online, Prev, Id) of {error, _Reason} -> ?DebugF("No online users (~p,~p), ets table was ~p ~n",[Id, Prev,ets:info(Online)]), {reply, {error, no_online}, State}; {ok, {User,Pwd}} -> ?DebugF("Choose user defined online user ~p for ~p ~n",[User, Id]), {reply, {ok, {User,Pwd}}, State#state{last_online={User,Pwd}}}; {ok, Next} -> ?DebugF("Choose online user ~p for ~p ~n",[Next, Id]), {reply, {ok, Next}, State#state{last_online=Next}} end; handle_call(stop, _From, State)-> {stop, normal, ok, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- %%Get one id in the full list of potential users handle_cast({init_seed, Val}, State) -> ts_utils:init_seed(Val), {noreply, State}; handle_cast({remove_connected, Id}, State=#state{online=Online,offline=Offline,connected=Connected}) -> %% session config may not include presence:final, so we need to check/delete from Online to be safe {noreply, LastOnline} = ets_delete_online(Online,Id,State), ets:delete(Connected,Id), ets:insert(Offline, {Id,2}), case {State#state.last_offline,ets:first(Offline)} of {undefined, Id} -> %% if we don't set last_offline, the next get_idle will %% respond with Id again. If possible we prefer to use %% another offline user {noreply, State#state{last_online=LastOnline, last_offline=Id}}; _Else -> {noreply, State#state{last_online=LastOnline}} end; %% user_defined user case handle_cast({add_to_connected, Id}, State=#state{connected=Connected, offline=Offline,first_client=First}) -> ets:insert(Connected, {Id,1}), NextOffline = ets_iterator_del(Offline, Id, State#state.last_offline), case First of undefined -> {noreply, State#state{last_connected=Id, first_client=Id, last_offline=NextOffline}}; _ -> {noreply, State#state{last_connected=Id,last_offline=NextOffline}} end; handle_cast({add_to_online, Id}, State=#state{online=Online, connected=Connected}) -> ?DebugF("add_to_online ~p~n",[Id]), case ets:member(Connected,Id) of true -> ets:delete(Connected,Id), ets:insert(Online, {Id,1}), {noreply, State#state{last_online=Id}}; false -> ?LOGF("add_to_online: warn, id ~p is not connected,do not add to online~n",[Id],?NOTICE), {noreply, State} end; handle_cast({remove_from_online, Id}, State=#state{online=Online,connected=Connected}) -> ?DebugF("remove_from_online ~p~n",[Id]), {noreply, LastOnline} = ets_delete_online(Online,Id,State), ets:insert(Connected, {Id,1}), {noreply, State#state{last_online=LastOnline}}; handle_cast({set_random_fileid, Id}, State) -> ?LOGF("Set file_server id for random users to ~p~n",[Id],?INFO), {noreply, State#state{random_server_id=Id}}; handle_cast({set_offline_fileid, Id}, State) -> ?LOGF("Set file_server id for offline users to ~p~n",[Id],?INFO), {noreply, State#state{offline_server_id=Id}}; handle_cast({set_fileid_delimiter, D}, State) -> ?LOGF("Set file_server delimiter ~p~n",[D],?DEB), {noreply, State#state{delimiter=D}}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%---------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%---------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- fill_offline(0, _, {undefined,_})-> ?LOG("no offline user defined",?DEB), ok; fill_offline(0, Offline, {FileId, Delimiter})-> ?LOGF("fill offline from file id ~p",[FileId],?DEB), case ts_file_server:get_all_lines(FileId) of {ok, Data} -> ?DebugF("offline user from csv ~p",[Data]), Fun = fun(Line) -> [User,Pwd| _]= ts_utils:split(Line,Delimiter), ets:insert(Offline,{{binary_to_list(User),binary_to_list(Pwd)},1}) end, lists:foreach(Fun, Data), ok; Error -> ?LOGF("error no offline user from csv~p",[Error],?DEB), ok end; fill_offline(N, Tab, Opts) when is_integer(N) -> ets:insert(Tab,{N, 0}), fill_offline(N-1, Tab, Opts). %%%---------------------------------------------------------------------- %%% Func: ets_iterator_del/3 %%% Args: Ets, Key, Iterator %%% Purpose: delete entry Key from Ets, update iterator if needed %%% Returns: Key:: integer | {string(),string()}|undefined %%%---------------------------------------------------------------------- %% iterator equal key:it will no longer be valid ets_iterator_del(Ets, Key, Key) -> Last = ets:prev(Ets,Key), ets:delete(Ets,Key), case Last of '$end_of_table' -> case ets:first(Ets) of '$end_of_table' -> undefined; Key -> undefined; NewIter -> NewIter end; NewIter -> NewIter end; ets_iterator_del(Ets, Key, Iterator) -> ets:delete(Ets,Key), Iterator. %%%---------------------------------------------------------------------- %%% Func: ets_iterator_next/2 %%% Args: Ets, Iterator %%% Purpose: get next key; no requirements on value %%% Returns: {ok, NextKey} or {error, empty_ets} %%%---------------------------------------------------------------------- ets_iterator_next(Ets, Iterator) -> ets_iterator_next(Ets, Iterator, undefined). %%%---------------------------------------------------------------------- %%% Func: ets_iterator_next/3 %%% Args: Ets, Iterator, Key %%% Purpose: get next key, should be different from 'Key', if possible %%%---------------------------------------------------------------------- ets_iterator_next(Ets, undefined, Key) -> case ets:first(Ets) of '$end_of_table' -> {error, empty_ets}; Key -> case ets:next(Ets,Key) of '$end_of_table' -> %% Key is the only entry of offline table {ok, Key}; Iter -> {ok, Iter} end; NewIter -> {ok, NewIter} end; ets_iterator_next(Ets, Iterator, Key) -> case ets:next(Ets,Iterator) of '$end_of_table' -> %% start again from the beginning ets_iterator_next(Ets, undefined, Key); Key -> % not this one, try again ets_iterator_next(Ets, Key, Key); Next -> {ok, Next} end. %%%---------------------------------------------------------------------- %%% Func: ets_delete_online/3 %%% Purpose: verify user is in Online table, delete, and update last_online if necessary %%%---------------------------------------------------------------------- ets_delete_online(Online,Id,State) -> case ets:lookup(Online,Id) of [] -> {noreply, State#state.last_online}; [_|_] -> LastOnline = ets_iterator_del(Online,Id,State#state.last_online), %% reset the last_online entries if it's equal to Id case State#state.last_online of Id -> ?LOGF("Reset last id (~p) because its offline ~n",[Id],?INFO), {noreply, LastOnline}; _ -> {noreply, State#state.last_online} end end. tsung-1.8.0/src/tsung_controller/tsung_controller.erl0000644000201100017670000001513314377756736022702 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(tsung_controller). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([start/0, start/2, start_phase/3, stop/1, stop_all/1, status/1, status_str/0]). -behaviour(application). -include("ts_macros.hrl"). -include_lib("kernel/include/file.hrl"). %% start the application with it's dependencies start() -> ts_utils:ensure_all_started(tsung_controller, permanent). %%---------------------------------------------------------------------- %% Func: start/2 %% Returns: {ok, Pid} | %% {ok, Pid, State} | %% {error, Reason} %%---------------------------------------------------------------------- start(_Type, _StartArgs) -> error_logger:tty(false), case ts_utils:setsubdir(?config(log_dir)) of {error, {error, eacces} } -> Msg = "Error while creating log directory in ~s: permission denied~n" , io:format(standard_error,Msg,[?config(log_dir)]), halt(77); {error, Err} -> Msg = "Error while creating log directory : ~s~n" , io:format(standard_error,Msg,[Err]), halt(1); {ok, {LogDir, _Name}} -> io:format(standard_io,"Log directory is: ~s~n", [LogDir]), application:set_env(tsung_controller,log_dir_real,LogDir), LogFile = filename:join(LogDir, atom_to_list(node()) ++ ".log"), case error_logger:logfile({open, LogFile }) of ok -> case ts_controller_sup:start_link(LogDir) of {ok, Pid} -> {ok, Pid}; Error -> io:format(standard_error,"Can't start ! ~p ~n",[Error]), halt(1) end; {error, Reason} -> Msg = "Error while opening log file: " , io:format(standard_error,Msg ++ " ~p ~n",[Reason]), halt(1) end end. start_phase(load_config, _StartType, _PhaseArgs) -> {Conf,Timeout} = case ?config(config_file) of "-" -> {standard_io, 120000}; %2mn timeout File -> T = case file:read_file_info(File) of {ok, #file_info{size=Size}} when Size > 10000000 -> % > 10MB io:format(standard_error,"Can take up to 5mn to read config file of size ~p~n ",[Size]), 300000; % 5mn {ok, #file_info{size=Size}} when Size > 1000000 -> % > 1MB io:format(standard_error,"Can take up to 3mn to read config file of size ~p~n ",[Size]), 180000; % 3mn {ok, #file_info{size=_}} -> 120000 % 2mn end, {File, T} end, case ts_config_server:read_config(Conf,Timeout) of {error,Reason}-> io:format(standard_error,"Config Error, aborting ! ~p~n ",[Reason]), init:stop(1); ok -> ok end; start_phase(start_os_monitoring, _StartType, _PhaseArgs) -> ts_os_mon:activate(); start_phase(start_clients, _StartType, _PhaseArgs) -> ts_mon:start_clients({?config(clients), ?config(dump), ?config(stats_backend)}). %%---------------------------------------------------------------------- %% Func: status/1 %% Returns: any %%---------------------------------------------------------------------- status([Host]) when is_atom(Host)-> _List = net_adm:world_list([Host]), Msg = case global:sync() of ok -> status_str(); {error, _Reason} -> "No status available: Can't synchronize with Tsung erlang nodes" end, io:format("~s~n",[Msg]). status_str()-> case catch ts_mon:status() of {Clients, Count, Connected, Interval, Phase, _Cpu} -> S1 = io_lib:format("Tsung is running [OK]~n" ++ " Current request rate: ~.2f req/sec~n" ++ " Current users: ~p~n" ++ " Current connected users: ~p ~n", [Count/Interval, Clients, Connected]), {ok, Nodes, Ended_Beams, _} = ts_config_server:status(), case {Phase, Nodes == Ended_Beams} of {error, _} -> % newphase not initialised, first phase S1 ++ " Current phase: 1"; {_, true} -> S1 ++ " Current phase: last, waiting for pending clients"; {{ok,P}, _} -> NPhases = (P div Nodes) + 1, io_lib:format("~s Current phase: ~p",[S1,NPhases]) end; {'EXIT', {noproc, _}} -> "Tsung is not started"; {'EXIT', _Else} -> "No status: Can't communicate with Tsung controller " end. %%---------------------------------------------------------------------- %% Func: stop/1 %% Returns: any %%---------------------------------------------------------------------- stop(_State) -> stop. %%---------------------------------------------------------------------- %% Func: stop_all/0 %% Returns: any %%---------------------------------------------------------------------- stop_all(Arg) -> ts_utils:stop_all(Arg,'ts_mon'). tsung-1.8.0/src/tsung_controller/ts_timer.erl0000644000201100017670000001717214377756736021132 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_timer). -vc('$Id$ '). -author('jflecomte@IDEALX.com'). -modifiedby('nicolas@niclux.org'). -include("ts_macros.hrl"). -behaviour(gen_fsm). %% Puropose: %% gen_fsm with 3 states: receiver, ack, initialize %% External events: connected %% External exports -export([start/1, connected/1, config/1, set_timeout/1]). %% gen_fsm callbacks -export([init/1, initialize/2, receiver/2, ack/2, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). -record(state, {nclient = 0, % number of unacked clients maxclients = 0, % total number of clients to ack pidlist = [], timeout=?config(global_ack_timeout)}). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(NClients) -> ?LOG("starting fsm timer",?INFO), gen_fsm:start_link({global, ?MODULE}, ?MODULE, [NClients], []). config(NClients) -> ?LOGF("Configure fsm timer with ~p",[NClients],?INFO), gen_fsm:send_event({global, ?MODULE}, {config, NClients}). set_timeout(Timeout) -> ?LOGF("Configure fsm timer timeout to with ~p",[Timeout],?INFO), gen_fsm:send_event({global, ?MODULE}, {set_timeout, Timeout}). connected(Pid) -> gen_fsm:send_event({global, ?MODULE}, {connected, Pid}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, StateName, StateData} | %% {ok, StateName, StateData, Timeout} | %% ignore | %% {stop, StopReason} %%---------------------------------------------------------------------- init(_Args) -> State= #state{}, ?LOGF("starting timer with timeout ~p",[State#state.timeout],?INFO), {ok, initialize, State}. %%---------------------------------------------------------------------- %% Func: StateName/2 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- initialize({config, Val}, State) -> {next_state, receiver, State#state{nclient=Val, maxclients=Val}}; initialize({set_timeout, Val}, State) -> {next_state, initialize, State#state{timeout=Val}}. receiver({set_timeout, Val}, State) -> {next_state, receiver, State#state{timeout=Val}}; %% now all the clients are connected, let's start to ack them receiver({connected, Pid}, State=#state{pidlist=List, nclient=1}) -> ?LOG("All connected, global ack!",?NOTICE), {next_state, ack, State#state{pidlist=[Pid|List],nclient=0}, 1}; %% receive a new connected mes receiver({connected, Pid}, State=#state{pidlist=List, nclient=N}) -> ?LOGF("New connected ~p (nclient=~p)",[Pid, N],?DEB), {next_state, receiver, State#state{pidlist=List ++ [Pid], nclient=N-1}, State#state.timeout}; %% timeout event, now we start to send ack, by sending a timeout event immediately receiver(timeout, StateData) -> {next_state, ack, StateData,1}. %% no more ack to send, stop ack(timeout, #state{pidlist=[]}) -> {stop, normal, #state{}}; %% ack all pids ack(timeout, State=#state{pidlist=L, maxclients = Max}) -> lists:foreach(fun(A)->ts_client:next({A}) end, L), {next_state, receiver, State#state{pidlist=[], nclient = Max}}. %%---------------------------------------------------------------------- %% Func: StateName/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: handle_event/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_sync_event/4 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_info/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm %% Returns: any %%---------------------------------------------------------------------- terminate(_Reason, _StateName, _StateData) -> ?LOG("terminate timer",?INFO), ok. %%-------------------------------------------------------------------- %% Func: code_change/4 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- tsung-1.8.0/src/tsung_controller/ts_stats_mon.erl0000644000201100017670000004143014377756736022013 0ustar nniclausdream%%% %%% Copyright (C) 2007 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%---------------------------------------------------------------------- %% @copyright 2007-2008 Nicolas Niclausse %% @author Nicolas Niclausse %% @since 20 Nov 2007 %% @doc computes statistics for request, page, connect, transactions, %% data size, errors, ... and other stats specific to plugins %% ---------------------------------------------------------------------- -module(ts_stats_mon). -author('nicolas@niclux.org'). -vc('$Id: ts_mon.erl 774 2007-11-20 09:36:13Z nniclausse $ '). -behaviour(gen_server). -include("ts_config.hrl"). %% External exports, API -export([start/0, start/1, stop/0, stop/1, add/1, add/2, dumpstats/0, dumpstats/1, set_output/2, set_output/3, status/1, status/2 ]). %% More external exports for ts_mon -export([update_stats/3, add_stats_data/2, reset_all_stats/1]). -export([print_stats/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {log, % log fd backend, % type of backend: text|rrdtool|fullstats dump_interval,% type = ts_stats_mon, % type of stats fullstats, % fullstats fd stats, % dict keeping stats info laststats % values of last printed stats }). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% @spec start(Id::term()) -> ok | throw({error, Reason}) %% @doc Start the monitoring process %%---------------------------------------------------------------------- start(Id) -> ?LOGF("starting ~p stats server, global ~n",[Id],?INFO), gen_server:start_link({global, Id}, ?MODULE, [Id], []). start() -> ?LOG("starting stats server, global ~n",?INFO), gen_server:start_link({global, ?MODULE}, ?MODULE, [?MODULE], []). stop(Id) -> gen_server:cast({global, Id}, {stop}). stop() -> gen_server:cast({global, ?MODULE}, {stop}). add([]) -> ok; add(Data) -> gen_server:cast({global, ?MODULE}, {add, Data}). add([], _Id) -> ok; add(Data, Id) -> gen_server:cast({global, Id}, {add, Data}). status(Name,Type) -> gen_server:call({global, ?MODULE}, {status, Name, Type}). status(Id) -> gen_server:call({global, Id}, {status}). dumpstats() -> gen_server:cast({global, ?MODULE}, {dumpstats}). dumpstats(Id) -> gen_server:cast({global, Id}, {dumpstats}). set_output(BackEnd,Stream) -> set_output(BackEnd,Stream,?MODULE). set_output(BackEnd,Stream,Id) -> gen_server:cast({global, Id}, {set_output, BackEnd, Stream}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- %% single type of data: don't need a dict, a simple list can store the data init([Type]) when Type == 'connect'; Type == 'page'; Type == 'request' -> ?LOGF("starting dedicated stats server for ~p ~n",[Type],?INFO), Stats = [0,0,0,0,0,0,0,0], {ok, #state{ dump_interval = ?config(dumpstats_interval), stats = Stats, type = Type, laststats = Stats }}; %% id = transaction or ?MODULE: it can handle several types of stats, must use a dict. init([Id]) -> ?LOGF("starting ~p stats server~n",[Id],?INFO), Tab = dict:new(), {ok, #state{ dump_interval = ?config(dumpstats_interval), stats = Tab, type = Id, laststats = Tab }}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call({status}, _From, State=#state{stats=Stats} ) when is_list(Stats) -> [_Esp, _Var, _Min, _Max, Count, _MeanFB,_CountFB,_Last] = Stats, {reply, Count, State}; handle_call({status}, _From, State=#state{stats=Stats} ) -> {reply, Stats, State}; handle_call({status, Name, Type}, _From, State ) -> Value = dict:find({Name,Type}, State#state.stats), {reply, Value, State}; handle_call(Request, _From, State) -> ?LOGF("Unknown call ~p !~n",[Request],?ERR), Reply = ok, {reply, Reply, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast({add, Data}, State=#state{type=Type}) when (( Type == 'connect') or (Type == 'page') or (Type == 'request')) -> case State#state.backend of fullstats -> io:format(State#state.fullstats,"~p~n",[{sample, Type, Data}]); _Other -> ok end, [Esp, Var, Max, Min, I, MeanFB,CountFB,Last] = State#state.stats, {NewEsp,NewVar,NewMin,NewMax,NewI} = ts_stats:meanvar_minmax(Esp,Var,Min,Max,Data,I), {noreply,State#state{stats=[NewEsp,NewVar,NewMax,NewMin,NewI,MeanFB,CountFB,Last]}}; handle_cast({add, Data}, State) when is_list(Data) -> case State#state.backend of fullstats -> io:format(State#state.fullstats,"~p~n",[Data]); _Other -> ok end, NewStats = lists:foldl(fun add_stats_data/2, State#state.stats, Data ), {noreply,State#state{stats=NewStats}}; handle_cast({add, Data}, State) when is_tuple(Data) -> case State#state.backend of fullstats -> io:format(State#state.fullstats,"~p~n",[Data]); _Other -> ok end, NewStats = add_stats_data(Data, State#state.stats), {noreply,State#state{stats=NewStats}}; handle_cast({set_output, BackEnd, {Stream, StreamFull}}, State) -> {noreply,State#state{backend=BackEnd, log=Stream, fullstats=StreamFull}}; handle_cast({dumpstats}, State) -> export_stats(State), NewStats = reset_all_stats(State#state.stats), {noreply, State#state{laststats = NewStats, stats=NewStats}}; handle_cast({stop}, State) -> {stop, normal, State}; handle_cast(Msg, State) -> ?LOGF("Unknown msg ~p !~n",[Msg], ?WARN), {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(Reason, State) -> ?LOGF("stopping stats monitor (~p)~n",[Reason],?INFO), export_stats(State), ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateData, _Extra) -> {ok, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: add_stats_data/2 %% Purpose: update or add value in dictionary %% Returns: Dict %%---------------------------------------------------------------------- %% continuous incrementing counters add_stats_data({Type, Name, Value},Stats) when Type==sample; Type==sample_counter -> MyFun = fun (OldVal) -> update_stats(Type, OldVal, Value) end, dict:update({Name,Type}, MyFun, update_stats(Type, [], Value), Stats); %% increase by one when called add_stats_data({count, Name}, Stats) -> dict:update_counter({Name, count}, 1, Stats); %% cumulative counter add_stats_data({sum, Name, Val}, Stats) -> dict:update_counter({Name, sum}, Val, Stats). %%---------------------------------------------------------------------- %% Func: export_stats/2 %%---------------------------------------------------------------------- export_stats(State=#state{type=Type,backend=Backend}) when Type == 'connect'; Type == 'page'; Type == 'request' -> Param = {Backend,State#state.laststats,State#state.log}, print_stats({Type,sample}, State#state.stats, Param); export_stats(State=#state{backend=Backend}) -> Param = {Backend,State#state.laststats,State#state.log}, dict:fold(fun print_stats/3, Param, State#state.stats). %%---------------------------------------------------------------------- %% @spec print_stats({Backend::tuple(),Name::tuple(), %% Type::sample|count|sum|sample_counter}, Value::list(), %% {Last, Logfile} ) -> {Last, Logfile} %% @doc print statistics in text format in Logfile @end %%---------------------------------------------------------------------- print_stats({_,_}, [], {Backend,LastRes,Logfile})-> {Backend,LastRes, Logfile}; print_stats({_,_}, [0,0,0,0,0,0,0|_], {Backend,LastRes,Logfile})-> {Backend,LastRes, Logfile}; print_stats({_,_,_}, 0, {Backend,0, Logfile})-> % no data yet {Backend,0, Logfile}; print_stats({{Name,Node},Type},Value,{json,Res,Log}) when (Type =:= sample) orelse (Type =:= sample_counter) -> Host = case string:tokens(Node,"@") of [_,HostName] -> HostName; [HostName] -> HostName end, print_stats_txt({Name,Type,", {\"name\": \"~p\", \"hostname\": \"" ++ Host ++"\", \"value\": ~p, \"mean\": ~p,\"stddev\": ~p,\"max\": ~p,\"min\": ~p ,\"global_mean\": ~p ,\"global_count\": ~p}"},Value,{json,Res,Log}); print_stats({Name,Type},Value,{json,Res,Log}) when (Type =:= sample) orelse (Type =:= sample_counter) -> print_stats_txt({Name,Type,", {\"name\": \"~p\", \"value\": ~p, \"mean\": ~p,\"stddev\": ~p,\"max\": ~p,\"min\": ~p ,\"global_mean\": ~p ,\"global_count\": ~p}"},Value,{json,Res,Log}); print_stats({Name,Type},Value,Other) when Type =:= sample orelse Type =:= sample_counter -> print_stats_txt({Name,Type,"stats: ~p ~p ~p ~p ~p ~p ~p ~p~n"},Value,Other); print_stats({Name,Type},Value,{json,Res,Log}) when is_integer(Name) -> % http return code print_stats_txt({Name,Type, ", {\"name\": \"http_~p\", \"value\": ~p, \"total\": ~p}"},Value,{json,Res,Log}); print_stats({Name=connected,Type},Value,{json,Res,Log}) -> print_stats_txt({Name,Type,", {\"name\": \"~p\", \"value\": ~p, \"max\": ~p}"},Value,{json,Res,Log}); print_stats({Name,Type},Value,{json,Res,Log}) -> print_stats_txt({Name,Type,", {\"name\": \"~p\", \"value\": ~p, \"total\": ~p}"},Value,{json,Res,Log}); print_stats({Name,Type},Value,Other) -> print_stats_txt({Name,Type,"stats: ~p ~p ~p~n"},Value,Other). %% @spec print_stats_txt(tuple(),Data::list(),tuple()) -> {Backend::atom(),LastRest::term(), LogFile::term()} print_stats_txt({Name,_,Format}, [Mean,0,Max,Min,Count,MeanFB,CountFB|_], {Backend,LastRes,Logfile})-> io:format(Logfile, Format, [Name, Count, Mean, 0, Max, Min,MeanFB,CountFB ]), {Backend,LastRes, Logfile}; print_stats_txt({Name,_,Format},[Mean,Var,Max,Min,Count,MeanFB,CountFB|_],{Backend,LastRes,Logfile})-> StdDev = math:sqrt(Var/Count), io:format(Logfile, Format, [Name, Count, Mean, StdDev, Max, Min, MeanFB,CountFB]), {Backend,LastRes, Logfile}; print_stats_txt({Name, _,Format}, [Value,Last], {Backend,LastRes, Logfile}) -> io:format(Logfile, Format, [Name, Value, Last ]), {Backend,LastRes, Logfile}; print_stats_txt({Name, _,Format}, Value, {Backend,LastRes, Logfile}) when is_number(LastRes)-> io:format(Logfile, Format, [Name, Value-LastRes, Value]), {Backend,LastRes, Logfile}; print_stats_txt({Name, Type, Format}, Value, {Backend,LastRes, Logfile}) when is_number(Value)-> PrevVal = case dict:find({Name, Type}, LastRes) of {ok, OldVal} -> OldVal; error -> 0 end, io:format(Logfile, Format, [Name, Value-PrevVal, Value]), {Backend,LastRes, Logfile}. %%---------------------------------------------------------------------- %% update_stats/3 %% @spec (Type::atom, List, Value::[integer() | float()]) -> List %% @doc update the mean and variance for the given sample %%---------------------------------------------------------------------- update_stats(sample, [], New) -> [New, 0, New, New, 1, 0, 0, 0]; update_stats(sample, Data, Value) -> %% we don't use lastvalue for 'sample', set it to zero update_stats2(Data, Value, 0); update_stats(sample_counter,[], New) -> %% first call, store the initial value [0, 0, 0, 0, 0, 0, 0, New]; update_stats(sample_counter, Current, 0) -> % skip 0 values Current; update_stats(sample_counter,[Mean,Var,Max,Min,Count,MeanFB,CountFB,Last],Value) when Value < Last-> %% maybe the counter has been restarted, use the new value, but don't update other data [Mean,Var,Max,Min,Count,MeanFB,CountFB,Value]; update_stats(sample_counter, [0, 0, 0, 0, 0, MeanFB,CountFB,Last], Value) -> New = Value-Last, [New, 0, New, New, 1, MeanFB,CountFB,Value]; update_stats(sample_counter,Data, Value) -> update_stats2(Data, Value, Value). update_stats2([Mean, Var, Max, Min, Count, MeanFB,CountFB,Last], Value, NewLast) when is_number(Value), is_number(NewLast), is_number(Last), is_number(Count)-> New = Value-Last, {NewMean, NewVar, _} = ts_stats:meanvar(Mean, Var, [New], Count), if New > Max -> % new max, min unchanged [NewMean, NewVar, New, Min, Count+1, MeanFB,CountFB,NewLast]; New < Min -> [NewMean, NewVar, Max, New, Count+1, MeanFB,CountFB,NewLast]; true -> [NewMean, NewVar, Max, Min, Count+1, MeanFB,CountFB,NewLast] end. %%---------------------------------------------------------------------- %% Func: reset_all_stats/1 %%---------------------------------------------------------------------- reset_all_stats(Data) when is_list(Data)-> reset_stats(Data); reset_all_stats(Dict)-> MyFun = fun (_Key, OldVal) -> reset_stats(OldVal) end, dict:map(MyFun, Dict). %%---------------------------------------------------------------------- %% @spec reset_stats(list()) -> list() %% @doc reset all stats except min and max and lastvalue. Compute the %% global mean here %% @end %%---------------------------------------------------------------------- reset_stats([]) -> []; reset_stats([_Mean, _Var, Max, Min, 0, _MeanFB,0,Last]) -> [0, 0, Max, Min, 0, 0, 0,Last]; reset_stats([Mean, _Var, Max, Min, Count, MeanFB,CountFB,Last]) -> NewCount=CountFB+Count, NewMean=(CountFB*MeanFB+Count*Mean)/NewCount, [0, 0, Max, Min, 0, NewMean,NewCount,Last]; reset_stats([_Sample, LastValue]) -> [0, LastValue]; reset_stats(LastValue) -> LastValue. tsung-1.8.0/src/tsung_controller/ts_os_mon_sup.erl0000644000201100017670000000531114377756736022163 0ustar nniclausdream%%% Copyright (C) 2009 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_os_mon_sup). -vc('$Id: ts_client_sup.erl 953 2008-11-23 16:57:05Z nniclausse $ '). -author('nicolas.niclausse@niclux.org'). -behaviour(supervisor). -include("ts_macros.hrl"). %% External exports -export([start_link/1, start_child/2]). %% supervisor callbacks -export([init/1]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link(Plugin) -> Module=get_module(Plugin), supervisor:start_link({local,Module}, ?MODULE, [Plugin]). start_child(Plugin, Args) -> ?LOGF("Starting child for plugin ~p with args ~p~n",[Plugin,Args], ?DEB), Module=get_module(Plugin), supervisor:start_child(Module,[Args]). %%%---------------------------------------------------------------------- %%% Callback functions from supervisor %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, {SupFlags, [ChildSpec]}} | %% ignore | %% {error, Reason} %%---------------------------------------------------------------------- init([Plugin]) -> ?LOGF("Starting with args ~p~n",[Plugin], ?INFO), SupFlags = {simple_one_for_one, 20, 20}, {ok, {SupFlags, get_spec(Plugin)}}. %% internal funs get_spec(Plugin) -> Module=get_module(Plugin), [{Module,{Module, start, []}, permanent, 2000, worker,[Module]}]. get_module(Plugin) when is_atom(Plugin)-> ModuleStr="ts_os_mon_" ++ atom_to_list(Plugin), list_to_atom(ModuleStr). tsung-1.8.0/src/tsung_controller/ts_os_mon_snmp.erl0000644000201100017670000002535614377756736022344 0ustar nniclausdream%%% %%% Copyright 2008 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 21 oct 2008 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_os_mon_snmp). -vc('$Id: ts_os_mon_snmp.erl,v 0.0 2008/10/21 12:57:49 nniclaus Exp $ '). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). -include("ts_macros.hrl"). -include("ts_os_mon.hrl"). -include_lib("snmp/include/snmp_types.hrl"). -export([start/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state,{ mon_server, % pid of mon server dnscache=[], interval, pid, % pid of snmp_mgr uri, % use as an identifier for the snmp manager host, oids = [], % custom OIDS funs, % store fun to be applied in a dict version, port, community, addr }). %% SNMP definitions %% FIXME: make this customizable in the XML config file ? -define(SNMP_CPU_RAW_USER, [1,3,6,1,4,1,2021,11,50,0]). -define(SNMP_CPU_RAW_SYSTEM, [1,3,6,1,4,1,2021,11,52,0]). -define(SNMP_CPU_RAW_IDLE, [1,3,6,1,4,1,2021,11,53,0]). -define(SNMP_CPU_LOAD1, [1,3,6,1,4,1,2021,10,1,5,1]). -define(SNMP_MEM_BUFFER, [1,3,6,1,4,1,2021,4,14,0]). -define(SNMP_MEM_CACHED, [1,3,6,1,4,1,2021,4,15,0]). -define(SNMP_MEM_AVAIL, [1,3,6,1,4,1,2021,4,6,0]). -define(SNMP_MEM_TOTAL, [1,3,6,1,4,1,2021,4,5,0]). -define(SNMP_TIMEOUT,5000). start(Args) -> ?LOGF("starting os_mon_snmp with args ~p",[Args],?NOTICE), gen_server:start_link(?MODULE, Args, []). %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init({HostStr, {Port, Community, Version, DynOIDS}, Interval, MonServer}) -> {ok, IP} = inet:getaddr(HostStr, inet), Apps = application:loaded_applications(), case proplists:is_defined(snmp,Apps) of true -> ?LOG("SNMP manager already started~n", ?NOTICE); _ -> ?LOG("Initialize SNMP application~n", ?NOTICE), Res1= application:start(snmp), ?LOGF("Initialize SNMP manager: ~p~n", [Res1],?NOTICE), Res2=snmpm:start(), ?LOGF("Register SNMP manager: ~p~n",[Res2], ?NOTICE), Res3=snmpm:register_user("tsung",snmpm_user_default,undefined), ?LOGF("SNMP initialization: ~p~n", [Res3],?NOTICE) end, erlang:start_timer(5, self(), connect ), FunsL= [{?SNMP_CPU_RAW_SYSTEM,cpu_system,sample_counter,fun(X)-> X/(Interval/1000) end}, {?SNMP_CPU_RAW_USER,cpu_user,sample_counter,fun(X)-> X/(Interval/1000) end}, {?SNMP_MEM_AVAIL,freemem,sample,fun(X)-> X/1000 end}, {?SNMP_CPU_LOAD1,load,sample,fun(X)-> X/100 end}] ++ DynOIDS, OIDS=lists:map(fun({Oid,_Name,_Type,_Fun})-> Oid end,FunsL), Funs=lists:foldl(fun({Oid,Name,Type,Fun},Acc)->dict:store(Oid,{Name,Type,Fun},Acc) end,dict:new(),FunsL), ?LOGF("Starting SNMP mgr on ~p~n", [IP], ?DEB), {ok, #state{ mon_server = MonServer, host = HostStr, uri = "snmp://"++HostStr++":" ++ integer_to_list(Port), port = Port, addr = IP, oids = OIDS, funs = Funs, community = Community, version = Version, interval = Interval}}. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(Msg, State) -> {stop, {unknown_message, Msg}, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info({timeout,_Ref,connect},State=#state{uri=URI, addr=IP,port=Port,community=Community,version=Version}) -> ok = snmpm:register_agent("tsung",URI, [{engine_id,"myengine"}, {address,IP}, {port,Port}, {version,Version}, {community,Community}]), ?LOGF("SNMP mgr started; remote node is ~p~n", [URI],?INFO), erlang:start_timer(State#state.interval, self(), send_request ), {noreply, State}; handle_info({timeout,_Ref,send_request},State=#state{uri=URI,oids=OIDS}) -> ?LOGF("SNMP mgr; get data from host ~p~n", [URI],?DEB), snmp_get(URI, OIDS, State), erlang:start_timer(State#state.interval, self(), send_request ), {noreply,State}; handle_info(Message, State) -> {stop, {unknown_message, Message} , State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: analyse_snmp_data/3 %% Returns: any (send msg to ts_mon) %%-------------------------------------------------------------------- analyse_snmp_data(Args, Host, State) -> analyse_snmp_data(Args, Host, [], State). %% Function: analyse_snmp_data/4 analyse_snmp_data([], _Host, Resp, State) -> ts_os_mon:send(State#state.mon_server,Resp); analyse_snmp_data([Val=#varbind{value='NULL'}| Tail], Host, Stats, State) -> ?LOGF("SNMP: Skip void result (~p) ~n", [Val],?DEB), analyse_snmp_data(Tail, Host, Stats, State); %% FIXME: this may not be accurate: if we lost packets (the server is %% overloaded), the value will be inconsistent, since we assume a %% constant time across samples ($INTERVAL) analyse_snmp_data([#varbind{variabletype='NULL'}| Tail], Host, Stats, State) -> %% skip bad values analyse_snmp_data(Tail, Host, Stats, State); analyse_snmp_data([#varbind{oid=?SNMP_CPU_RAW_SYSTEM, value=Val}| Tail], Host, Stats, State) -> {value, User} = lists:keysearch(?SNMP_CPU_RAW_USER, #varbind.oid, Tail), Value = Val + User#varbind.value, CountName = {cpu , Host}, NewValue = Value/(State#state.interval/1000), NewTail = lists:keydelete(?SNMP_CPU_RAW_USER, #varbind.oid, Tail), analyse_snmp_data(NewTail, Host, [{sample_counter, CountName, NewValue}| Stats], State); analyse_snmp_data([User=#varbind{oid=?SNMP_CPU_RAW_USER}| Tail], Host, Stats, State) -> %%put this entry at the end, this will be used when SYSTEM match analyse_snmp_data(Tail ++ [User], Host, Stats, State); analyse_snmp_data([#varbind{oid=OID, value=Val}| Tail], Host, Stats, State=#state{funs=F}) -> {DataName, Type, Fun} = dict:fetch(OID,F), Value = Fun(Val), Name = {DataName,Host}, ?LOGF("Analyse SNMP: ~p:~p:~p ~n", [Type, Name, Value],?DEB), analyse_snmp_data(Tail, Host, [{Type, Name, Value}| Stats], State). %%-------------------------------------------------------------------- %% Function: snmp_get/3 %% Description: ask a list of OIDs to the given snmp_mgr %%-------------------------------------------------------------------- snmp_get(Agent,OIDs,State)-> snmp_get(Agent,[OIDs],State,?SNMP_TIMEOUT,[]). snmp_get("snmp://"++Host, [], State, _TimeOut, Results )-> [Agent|_]=string:tokens(Host,":"), analyse_snmp_data(Results,Agent,State); snmp_get(URI, [OIDs|Tail], State, TimeOut,PrevRes)-> ?LOGF("Running snmp get ~p ~p~n", [URI,OIDs], ?DEB), Res = snmpm:sync_get("tsung",URI,OIDs,TimeOut), ?LOGF("Res ~p ~n", [Res], ?DEB), case Res of {ok,{noError,_,Results},_Remaining} -> snmp_get(URI, Tail, State, TimeOut, Results++PrevRes); {error, {send_failed,_,tooBig}} -> %% split the OID list in two, and retry ?LOGF("SNMP: too big packet, split and retry (~p)~n", [URI], ?INFO), snmp_get(URI, tuple_to_list(lists:split(length(OIDs) div 2, OIDs)), State, TimeOut, PrevRes); Other -> ?LOGF("SNMP Error:~p for ~p~n", [Other, URI], ?WARN), {error, Other} end. tsung-1.8.0/src/tsung_controller/ts_os_mon_munin.erl0000644000201100017670000002710114377756736022503 0ustar nniclausdream%%% %%% Copyright 2008 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 21 oct 2008 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_os_mon_munin). -vc('$Id: ts_os_mon_snmp.erl,v 0.0 2008/10/21 12:57:49 nniclaus Exp $ '). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% @doc munin plugin for ts_os_mon %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -include("ts_macros.hrl"). -include("ts_os_mon.hrl"). -define(READ_TIMEOUT,2500). % 2.5 sec -define(SEND_TIMEOUT,5000). -define(RETRY_SLEEP,30000). %% if interval is more than this, we must send ping to avoid closed %% connection from munin node server (default timeout is 10s in recent %% version of munin-node): -define(MAX_INTERVAL,8000). -define(PING_INTERVAL,5000). -export([start/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state,{ mon, % pid of mon server interval, % interval in msec between gathering of data socket, % tcp socket port, % tcp port of munin-node server host, % remote munin-node hostname addr, % remote munin-node IP addr ncpus % number of cpus of remote server }). start(Args) -> ?LOGF("starting os_mon_munin with args ~p",[Args],?NOTICE), gen_server:start_link(?MODULE, Args, []). %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init({HostStr, {Port}, Interval, MonServer}) -> ?LOGF("Starting munin mgr on ~p:~p~n", [HostStr,Port], ?DEB), {ok, IP} = inet:getaddr(HostStr, inet), erlang:start_timer(?INIT_WAIT, self(), connect ), {ok, #state{mon=MonServer, host=HostStr, interval=Interval, addr=IP, port=Port}}. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(Msg, State) -> {stop, {unknown_message, Msg}, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info({timeout,_Ref,connect},State=#state{addr=IP,port=Port,host=HostStr}) -> Opts=[list, {active, false}, {packet, line}, {send_timeout, ?SEND_TIMEOUT}, {keepalive, true} ], case gen_tcp:connect(IP, Port, Opts) of {ok, Socket} -> case gen_tcp:recv(Socket,0, ?READ_TIMEOUT) of {ok, "# munin node at "++ Str} -> MuninHost = ts_utils:chop(Str), ?LOGF("Connected to ~p~n", [MuninHost], ?INFO), %% We want CPU value ranging from 0 to 100, so we need the max value : gen_tcp:send(Socket,"config cpu\n"), ConfigCPU=read_munin_data(Socket), NCPUs = case proplists:get_value('user.max',ConfigCPU) of Num when is_number(Num) -> Num/100 ; _ -> ?LOG("can't find the number of CPU, assume one~n",?NOTICE), 1 end, ?LOGF("first fetch successful to ~p~n", [MuninHost], ?INFO), case (State#state.interval > ?MAX_INTERVAL) of true -> erlang:start_timer(?PING_INTERVAL, self(), ping ); _ -> ok end, erlang:start_timer(State#state.interval, self(), send_request ), {noreply, State#state{socket=Socket,host=MuninHost,ncpus=NCPUs}}; {error, Reason} -> ?LOGF("Error while connecting to munin server: ~p~n", [Reason], ?ERR), {stop, Reason, State} end; {error, Reason} -> ?LOGF("Can't connect to munin server on ~p, reason:~p~n", [HostStr, Reason], ?ERR), {stop, Reason, State} end; handle_info({timeout, _Ref, ping}, State=#state{socket=Socket} ) -> gen_tcp:send(Socket,"\n"), gen_tcp:recv(Socket,0,?READ_TIMEOUT), erlang:start_timer(?PING_INTERVAL, self(), ping ), {noreply, State}; handle_info({timeout, _Ref, send_request}, State=#state{socket=Socket,host=Hostname} ) -> %% Currently, fetch only cpu and memory %% FIXME: should be customizable in XML config file ?LOGF("Fetching munin for cpu on host ~p~n", [Hostname], ?DEB), gen_tcp:send(Socket,"fetch cpu\n"), AllCPU=read_munin_data(Socket), ?LOGF("Fetching munin for memory on host ~p~n", [Hostname], ?DEB), gen_tcp:send(Socket,"fetch memory\n"), AllMem=read_munin_data(Socket), ?LOGF("Fetching munin for load on host ~p~n", [Hostname], ?DEB), gen_tcp:send(Socket,"fetch load\n"), AllLoad=read_munin_data(Socket), %% sum all cpu types, except idle. NonIdle=lists:keydelete('idle.value',1,AllCPU), RawCpu = lists:foldl(fun({_Key,Val},Acc) when is_integer(Val)-> Acc+Val end,0,NonIdle) / (State#state.interval div 1000), Cpu=check_value(RawCpu,{Hostname,"cpu"})/State#state.ncpus, ?LOGF(" munin cpu on host ~p is ~p~n", [Hostname,Cpu], ?DEB), %% returns free + buffer + cache FunFree = fun({Key,Val},Acc) when ((Key=='buffers.value') or (Key=='free.value') or (Key=='cached.value') ) -> Acc+Val; (_, Acc) -> Acc end, FreeMem=check_value(lists:foldl(FunFree,0,AllMem),{Hostname,"memory"})/1048576,%MBytes ?LOGF(" munin memory on host ~p is ~p~n", [Hostname,FreeMem], ?DEB), %% load only has one value at present Load = lists:foldl(fun({_Key,Val},Acc) -> Acc+Val end,0,AllLoad), ?LOGF(" munin load on host ~p is ~p~n", [Hostname,Load], ?DEB), ts_os_mon:send(State#state.mon,[{sample_counter, {cpu, Hostname}, Cpu}, {sample, {freemem, Hostname}, FreeMem}, {sample, {load, Hostname}, Load}]), erlang:start_timer(State#state.interval, self(), send_request ), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, #state{socket=undefined}) -> ok; terminate(_Reason, #state{socket=Socket}) -> gen_tcp:close(Socket). %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- read_munin_data(Socket)-> read_munin_data(Socket,gen_tcp:recv(Socket,0,?READ_TIMEOUT),[]). read_munin_data(_Socket,{ok,".\n"}, Acc)-> Acc; read_munin_data(Socket,{ok, "graph_args --base "++ Data}, Acc) when is_list(Acc)-> %% special case for getting the number of cpus NewAcc = case re:run(Data,"--upper-limit (\\d+)",[{capture,all_but_first,list}]) of {match,[Val]} when length(Val) > 0 -> ?LOGF("the munin node has ~p CPUs ~n",[Val],?INFO), [{'user.max',list_to_integer(Val)}| Acc]; _ -> ?LOGF("upper-limit don't match ~p~n",[Data],?WARN), Acc end, read_munin_data(Socket,gen_tcp:recv(Socket,0,?READ_TIMEOUT), NewAcc); read_munin_data(Socket,{ok, Data}, Acc) when is_list(Acc)-> ?DebugF("Parse munin data: ~p~n",[Data]), NewAcc = case string:tokens(Data," \n") of [Key, Value] -> try ts_utils:list_to_number(Value) of Num when is_number(Num) -> [{list_to_atom(Key), Num }|Acc] catch _Type:_Exp -> Acc end; [_Key| _Rest] -> Acc; _ -> ?LOGF("Unknown data received from munin server: ~p~n",[Data],?WARN), Acc end, read_munin_data(Socket,gen_tcp:recv(Socket,0,?READ_TIMEOUT), NewAcc); read_munin_data(Socket,{error, timeout}, Acc) when is_list(Acc)-> %% the remote server may be overloaded, wait a bit before retrying ?LOG("munin: timeout error, server must be overloaded, sleep for 30 sec~n", ?WARN), gen_tcp:close(Socket), timer:sleep(?RETRY_SLEEP), erlang:error(server_timeout). %% check is this a valid value (positive at least) check_value(Val,_) when Val > 0 -> Val; check_value(Val,{Host, Type}) -> ?LOGF("munin: bad ~s value on host ~p: ~p~n", [Type, Host, Val],?WARN), 0. tsung-1.8.0/src/tsung_controller/ts_os_mon_erlang.erl0000644000201100017670000003360514377756736022633 0ustar nniclausdream%%% %%% Copyright 2008 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 21 oct 2008 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_os_mon_erlang). -vc('$Id: ts_os_mon_snmp.erl,v 0.0 2008/10/21 12:57:49 nniclaus Exp $ '). -author('nicolas.niclausse@niclux.org'). -include("ts_macros.hrl"). -include("ts_os_mon.hrl"). -export([start/1, updatestats/3, client_start/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(NODE, 'os_mon'). -define(PROCNET, "/proc/net/dev"). -record(state,{ mon, % pid of mon server interval, % interval node, % name of node to monitor host, % hostname of server to monitor pid, % remote pid retry=false,% have we retried to connect ? options % updatestats options }). start(Args) -> gen_server:start_link(?MODULE, Args, []). %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init( {Host, Options, Interval, MonServer} ) -> {ok, LocalHost} = ts_utils:node_to_hostname(node()), %% to get the EXIT signal from spawned processes on remote nodes process_flag(trap_exit,true), %% because the stats for cpu has to be called from the same %% process (otherwise the same value (mean cpu% since the system %% last boot) is returned by cpu_sup:util), we must spawn a process %% on the remote node that will do the stats collection and send it back %% to ts_mon case LocalHost of Host -> % same host, don't start a new beam ?LOG("Running os_mon on the same host as the controller, use the same beam~n",?INFO), application:start(sasl), application:start(os_mon), erlang:start_timer(?INIT_WAIT, self(), spawn), {ok, #state{node=node(),mon=MonServer, host=Host, interval=Interval, options=Options}}; _ -> erlang:start_timer(?INIT_WAIT, self(), start_beam), {ok, #state{host=Host, mon=MonServer, interval=Interval, options=Options}} end. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(Msg, State) -> ?LOGF("handle cast: unknown msg ~p~n",[Msg],?WARN), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info({timeout,_Ref,spawn},State=#state{mon=MonServer, options=Options, interval=Interval})-> Pid = spawn_link(?MODULE, updatestats, [Options, Interval, MonServer]), ?LOGF("Spawn monitoring process on localhost (~p)~n",[Pid],?INFO), {noreply, State#state{pid=Pid}}; handle_info({'EXIT',_Pid,noconnection},State=#state{retry=true, host=Host})-> ?LOGF("Fail to start beam on host ~p (try 2)~n", [Host],?ERR), {stop, normal, State}; handle_info({'EXIT',Pid,noconnection},State=#state{host=Host})-> ?LOGF("Fail to start beam on host ~p , retry ~n", [Host],?ERR), handle_info({timeout,Pid,start_beam},State#state{retry=true}); handle_info({timeout,_Ref,start_beam},State=#state{host=Host})-> case start_beam(Host) of {ok, Node} -> Pong = net_adm:ping(Node), ?LOGF("ping ~p: ~p~n", [Node, Pong],?INFO), load_code([Node]), Pid = spawn_link(Node, ?MODULE, updatestats, [State#state.options, State#state.interval, State#state.mon]), {noreply, State#state{node=Node,pid=Pid}}; {error,{already_running,Node}} -> ?LOGF("Node ~p is already running, start updatestats process~n", [Node],?NOTICE), Pid = spawn_link(Node, ?MODULE, updatestats, [State#state.options, State#state.interval, State#state.mon]), {noreply, State#state{node=Node,pid=Pid}}; Error -> ?LOGF("Fail to start beam on host ~p (~p)~n", [Host, Error],?ERR), {stop, normal, State} end. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %% Function: updatestats/3 %% Purpose: update stats for erlang monitoring. Executed on the remote host %%-------------------------------------------------------------------- updatestats(Options, Interval,Mon_Server) -> Node = atom_to_list(node()), {Cpu, FreeMem, RecvPackets, SentPackets, Load} = node_data(), Stats = [{sample, {cpu, Node}, Cpu}, {sample, {freemem, Node}, FreeMem}, {sample, {load, Node}, Load}, {sample_counter, {recvpackets, Node}, RecvPackets}, {sample_counter, {sentpackets, Node}, SentPackets}], Fun = fun(Option, AccumStats) -> case Option of {mysqladmin, MysqlOptions} -> {Threads, Questions} = mysqladmin_data(MysqlOptions), lists:append(AccumStats, [{sample, {mysql_threads, Node}, Threads}, {sample_counter, {mysql_questions, Node}, Questions}]); _ -> AccumStats end end, ts_os_mon:send(Mon_Server, lists:append(Stats, lists:foldl(Fun, [], Options))), timer:sleep(Interval), updatestats(Options, Interval,Mon_Server). %%-------------------------------------------------------------------- %% Function: client_start/0 %% Purpose: Start the monitor tools on the node that you want to spy on %%-------------------------------------------------------------------- client_start() -> application:start(stdlib), application:start(sasl), application:start(os_mon). %%-------------------------------------------------------------------- %% Function: load_code/1 %% Purpose: Load ts_os_mon code on all Erlang nodes %%-------------------------------------------------------------------- load_code(Nodes) -> ?LOGF("Loading tsung monitor on nodes ~p~n", [Nodes], ?NOTICE), LoadCode = fun(Mod)-> {_, Binary, _} = code:get_object_code(Mod), rpc:multicall(Nodes, code, load_binary, [Mod, Mod, Binary], infinity) end, LoadRes = lists:map(LoadCode, [ts_mon, ?MODULE, ts_os_mon, ts_utils]), Res = rpc:multicall(Nodes, ?MODULE, client_start, [], infinity), %% first value of load call is garbage ?LOGF("load_code: ~p start: ~p ~n", [LoadRes, Res],?DEB), ok. %%-------------------------------------------------------------------- %% Func: node_data/0 %%-------------------------------------------------------------------- node_data() -> {RecvPackets, SentPackets} = get_os_data(packets), {get_os_data(cpu), get_os_data(freemem), RecvPackets, SentPackets, get_os_data(load)}. %%-------------------------------------------------------------------- %% Func: mysqladmin_data/1 %%-------------------------------------------------------------------- mysqladmin_data({Port, Username, Password}) -> PasswdArg = case Password of false -> ""; _ -> io_lib:format("-p\"~s\"", [Password]) end, Cmd = io_lib:format("mysqladmin -u\"~s\" ~s -P~B status", [Username, PasswdArg, Port]), Result = os:cmd(Cmd), % Uptime: 1146892 Threads: 2 Questions: 15242050 Slow queries: 0 Opens: 176 Flush tables: 1 Open tables: 101 Queries per second avg: 13.290 [_, _Uptime, _, Threads, _, Questions, _, _, _SlowQueries, _, _Opens, _, _, _FlushTables, _, _, _OpenTables, _, _, _, _, _QPS] = string:tokens(Result, " \n"), {list_to_integer(Threads), list_to_integer(Questions)}. %%-------------------------------------------------------------------- %% Func: get_os_data/1 %%-------------------------------------------------------------------- %% Return node cpu utilisation get_os_data(cpu) -> cpu_sup:util(); %% Return node cpu average load on 1 minute; get_os_data(load) -> cpu_sup:avg1()/256; get_os_data(DataName) -> get_os_data(DataName,os:type()). %%-------------------------------------------------------------------- %% Func: get_os_data/2 %%-------------------------------------------------------------------- %% Return free memory in bytes. %% Use the result of the free commands on Linux and os_mon on all %% other platforms get_os_data(freemem, {unix, linux}) -> case file:open("/proc/meminfo",[raw,read]) of {ok, FD} -> file:read_line(FD), % skip MemTotal {ok, Data} = file:read_line(FD), ["MemFree:",RawFree,_] = string:tokens(Data," \n"), case file:read_line(FD) of {ok, "MemAvailable:" ++Tail} -> [Avail,_] = string:tokens(Tail," \n"), file:close(FD), list_to_integer(Avail)/1024; {ok, NextLine} -> ["Buffers:",Buf,_] = string:tokens(NextLine," \n"), {ok, LastLine} = file:read_line(FD), ["Cached:",Cached,_] = string:tokens(LastLine," \n"), file:close(FD), (list_to_integer(RawFree)+list_to_integer(Buf) + list_to_integer(Cached)) / 1024 end; _ -> 0 end; get_os_data(freemem, {unix, sunos}) -> Result = os:cmd("vmstat 1 2 | tail -1"), [_, _, _, _, Free | _] = string:tokens(Result, " "), list_to_integer(Free)/1024; get_os_data(freemem, _OS) -> Data = memsup:get_system_memory_data(), {value,{free_memory,FreeMem}} = lists:keysearch(free_memory, 1, Data), %% We use Megabytes FreeMem/1048576; %% Return packets sent/received on network interface get_os_data(packets, {unix, Val}) -> Data=os:cmd("netstat -in"), get_os_data(packets, {unix, Val},string:tokens(Data, "\n")); get_os_data(packets, _OS) -> {0, 0 }. % FIXME: not implemented for other arch. %% %% packets , special case with File as a variable for easy testing get_os_data(packets, {unix, _}, Data) -> %% lists:zf is called lists:filtermap in erlang R16B1 and newer Eth=[io_lib:fread("~d~d~d~d~d~d~d~d~d", X) || {E,X}<-ts_utils:filtermap(fun(Y)-> case string:chr(Y, $:) of 0 -> {true, ts_utils:split2(Y,32,strip)}; _ -> false end end , Data), string:str(E,"eth") /= 0], Fun = fun (A, {Rcv, Sent}) -> {ok,[_,_,RcvBytes,_,_,_,SentBytes,_,_],_}=A, {Rcv+RcvBytes,Sent+SentBytes} end, lists:foldl(Fun, {0,0}, Eth). %%-------------------------------------------------------------------- %% Function: start_beam/1 %% Purpose: Start an Erlang node on given host %%-------------------------------------------------------------------- start_beam(Host) -> Args = ts_utils:erl_system_args(), ?LOGF("Starting os_mon beam on host ~p ~n", [Host], ?NOTICE), ?LOGF("~p Args: ~p~n", [Host, Args], ?DEB), slave:start(list_to_atom(Host), ?NODE, Args). tsung-1.8.0/src/tsung_controller/ts_os_mon.erl0000644000201100017670000000630114377756736021274 0ustar nniclausdream%%% This code was developed by Mickael Remond %%% and contributors (their names can %%% be found in the CONTRIBUTORS file). Copyright (C) 2003 Mickael %%% Remond %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% Created : 23 Dec 2003 by Mickael Remond -module(ts_os_mon). -author('mickael.remond@erlang-fr.org'). -modifiedby('nicolas@niclux.org'). -vc('$Id$ '). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- -include("ts_macros.hrl"). -include("ts_os_mon.hrl"). %%-------------------------------------------------------------------- %% External exports -export([activate/0, send/2]). %%% send data back to the controlling node send(Mon_Server, Data) when is_pid(Mon_Server) -> Mon_Server ! {add, Data}; send(Mon_Server, Data) -> gen_server:cast(Mon_Server, {add, Data}). %%-------------------------------------------------------------------- %% Function: activate/0 %% Purpose: This is used by tsung to start the cluster monitor service %% It will only be started if there are cluster/monitor@host element %% in the config file. %%-------------------------------------------------------------------- activate() -> {ok, Controller} = ts_utils:node_to_hostname(node()), case ts_config_server:get_monitor_hosts() of [] -> ?LOG("Add monitoring of controller node",?DEB), ts_os_mon_sup:start_child(erlang, {Controller,[],?INTERVAL, {global,ts_mon}}), ok; Hosts -> NewHosts = case lists:keyfind(Controller, 1, Hosts) of false -> ?LOG("Force monitoring of controller node",?DEB), Hosts++[{Controller, {erlang,[]}}]; _ -> Hosts end, Fun = fun({HostStr,{Type,Options}}) -> Args= {HostStr, Options, ?INTERVAL,{global, ts_mon}}, ts_os_mon_sup:start_child(Type, Args) end, lists:foreach(Fun,NewHosts) end. tsung-1.8.0/src/tsung_controller/ts_msg_server.erl0000644000201100017670000001145214377756736022161 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. -module(ts_msg_server). -author('jflecomte@IDEALX.com'). -vc('$Id$ '). -export([get_id/0, get_id/1, reset/0]). -include("ts_macros.hrl"). -behaviour(gen_server). %% External exports -export([start/0, stop/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {number}). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start() -> ?LOG("Starting ~n",?INFO), gen_server:start_link({global,?MODULE}, ?MODULE, [], []). get_id()-> gen_server:call({global, ?MODULE}, get_id). get_id(list)-> integer_to_list(get_id()); get_id({_,_DynData}) -> % to use this fun in substitutions get_id(list). reset()-> gen_server:call({global, ?MODULE}, reset). stop()-> gen_server:call({global, ?MODULE}, stop). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init([]) -> {ok, #state{number = 0}}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call(get_id, _From, State) -> Element = State#state.number + 1, State2 = State#state{number = Element}, {reply, Element, State2}; handle_call(reset, _From, State) -> {reply, ok, State#state{number = 0}}; handle_call(stop, _From, State)-> {stop, normal, ok, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(Reason, _State) -> ?LOGF("terminate ~n (reason ~p)",[Reason],?INFO), ok. %%---------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%---------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- tsung-1.8.0/src/tsung_controller/ts_mon.erl0000644000201100017670000005406414377756736020604 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2004 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%---------------------------------------------------------------------- %% @copyright 2001 IDEALX %% @author Nicolas Niclausse %% @since 8 Feb 2001 %% %% @doc monitor and log events: arrival and departure of users, new %% connections, os_mon and send/rcv message (when dump is set to true) %%---------------------------------------------------------------------- -module(ts_mon). -author('nicolas@niclux.org'). -vc('$Id$ '). -behaviour(gen_server). -include("ts_config.hrl"). %% External exports -export([start/1, stop/0, newclient/1, endclient/1, sendmes/1, start_clients/1, abort/0, status/0, rcvmes/1, dumpstats/0, dump/1, launcher_is_alive/0 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(DUMP_FILENAME,"tsung.dump"). -define(FULLSTATS_FILENAME,"tsung-fullstats.log"). -define(DELAYED_WRITE_SIZE,524288). % 512KB -define(DELAYED_WRITE_DELAY,5000). % 5 sec %% one global ts_stats_mon procs + 4 dedicated stats procs to share the load -define(STATSPROCS, [request, connect, page, transaction, ts_stats_mon]). -record(state, {log, % log fd backend, % type of backend: text|... log_dir, % log directory fullstats, % fullstats fd dump_interval,% dumpfile, % file used when dumptrafic is set light or full client=0, % number of clients currently running maxclient=0, % max of simultaneous clients stats, % record keeping stats info stop = false, % true if we should stop laststats, % values of last printed stats lastdate, % date of last printed stats type, % type of logging (none, light, full) launchers=0, % number of launchers started timer_ref, % timer reference (for dumpstats) wait_gui=false% wait gui before stopping }). -record(stats, { users_count = 0, finish_users_count = 0, os_mon, session = [] }). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% @spec start(LogDir::string())-> {ok, Pid::pid()} | ignore | {error, Error::term()} %% @doc Start the monitoring process %% @end %%---------------------------------------------------------------------- start(LogDir) -> ?LOG("starting monitor, global ~n",?NOTICE), gen_server:start_link({global, ?MODULE}, ?MODULE, [LogDir], []). %% @spec start_clients({Machines::term(), Dump::string(), BackEnd::atom()}) -> ok start_clients({Machines, Dump, BackEnd}) -> gen_server:call({global, ?MODULE}, {start_logger, Machines, Dump, BackEnd}, infinity). stop() -> gen_server:cast({global, ?MODULE}, {stop}). status() -> gen_server:call({global, ?MODULE}, {status}). abort() -> gen_server:cast({global, ?MODULE}, {abort}). dumpstats() -> gen_server:cast({global, ?MODULE}, {dumpstats}). newclient({Who, When}) -> gen_server:cast({global, ?MODULE}, {newclient, Who, When}). endclient({Who, When, Elapsed}) -> gen_server:cast({global, ?MODULE}, {endclient, Who, When, Elapsed}). sendmes({none, _, _}) -> skip; sendmes({protocol, _, _}) -> skip; sendmes({protocol_local, _, _}) -> skip; sendmes({_Type, Who, What}) -> gen_server:cast({global, ?MODULE}, {sendmsg, Who, ?TIMESTAMP, What}). rcvmes({none, _, _}) -> skip; rcvmes({protocol, _, _})-> skip; rcvmes({protocol_local, _, _})-> skip; rcvmes({_, _, closed}) -> skip; rcvmes({_Type, Who, What}) -> gen_server:cast({global, ?MODULE}, {rcvmsg, Who, ?TIMESTAMP, What}). dump({none, _, _})-> skip; dump({cached, << >> })-> skip; dump({_Type, Who, What}) -> gen_server:cast({global, ?MODULE}, {dump, Who, ?TIMESTAMP, What}); dump({cached, Data})-> gen_server:cast({global, ?MODULE}, {dump, cached, Data}). launcher_is_alive() -> gen_server:cast({global, ?MODULE}, {launcher_is_alive}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init([LogDir]) -> ?LOGF("Init, log dir is ~p~n",[LogDir],?INFO), Stats = #stats{os_mon = dict:new()}, State=#state{ dump_interval = ?config(dumpstats_interval), log_dir = LogDir, stats = Stats, lastdate = ?NOW, laststats = Stats }, case ?config(mon_file) of "-" -> {ok, State#state{log=standard_io}}; Name -> Filename = filename:join(LogDir, Name), case file:open(Filename,[append]) of {ok, Stream} -> ?LOG("starting monitor~n",?INFO), {ok, State#state{log=Stream}}; {error, Reason} -> ?LOGF("Can't open mon log file! ~p~n",[Reason], ?ERR), {stop, Reason} end end. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call({start_logger, Machines, DumpType, Backend}, From, State) -> start_logger({Machines, DumpType, Backend}, From, State); %%% get status handle_call({status}, _From, State=#state{stats=Stats}) -> {ok, Localhost} = ts_utils:node_to_hostname(node()), CpuName = {{cpu,"tsung_controller@"++Localhost}, sample}, CPU = case dict:find(CpuName,Stats#stats.os_mon) of {ok, [ValCPU|_]} -> ValCPU ; _ -> 0 end, Request = ts_stats_mon:status(request), Interval = ts_utils:elapsed(State#state.lastdate, ?NOW) / 1000, Phase = ts_stats_mon:status(newphase,sum), Connected = case ts_stats_mon:status(connected,sum) of {ok, Val} -> Val; _ -> 0 end, Reply = { State#state.client, Request, Connected, Interval, Phase, CPU}, {reply, Reply, State}; handle_call(Request, _From, State) -> ?LOGF("Unknown call ~p !~n",[Request],?ERR), Reply = ok, {reply, Reply, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast({add, _Data}, State=#state{wait_gui=true}) -> {noreply,State}; handle_cast({add, Data}, State=#state{stats=Stats}) when is_list(Data) -> case State#state.backend of fullstats -> io:format(State#state.fullstats,"~p~n",[Data]); _Other -> ok end, New = lists:foldl(fun ts_stats_mon:add_stats_data/2, Stats#stats.os_mon, Data), NewStats = Stats#stats{os_mon=New}, {noreply,State#state{stats=NewStats}}; handle_cast({add, Data}, State=#state{stats=Stats}) when is_tuple(Data) -> case State#state.backend of fullstats -> io:format(State#state.fullstats,"~p~n",[Data]); _Other -> ok end, New = ts_stats_mon:add_stats_data(Data, Stats#stats.os_mon), NewStats = Stats#stats{os_mon=New}, {noreply,State#state{stats=NewStats}}; handle_cast({newclient, Who, When}, State=#state{stats=Stats}) -> Clients = State#state.client+1, OldCount = Stats#stats.users_count, NewStats = Stats#stats{users_count=OldCount+1}, case State#state.type of none -> ok; protocol -> ok; protocol_local -> ok; _ -> io:format(State#state.dumpfile,"NewClient:~w:~p~n",[ts_utils:time2sec_hires(When), Who]), io:format(State#state.dumpfile,"load:~w~n",[Clients]) end, case Clients > State#state.maxclient of true -> {noreply, State#state{client = Clients, maxclient=Clients, stats=NewStats}}; false -> {noreply, State#state{client = Clients, stats=NewStats}} end; handle_cast({endclient, Who, When, Elapsed}, State=#state{stats=Stats}) -> Clients = State#state.client-1, OldSession = Stats#stats.session, %% update session sample NewSession = ts_stats_mon:update_stats(sample, OldSession, Elapsed), OldCount = Stats#stats.finish_users_count, NewStats = Stats#stats{finish_users_count=OldCount+1,session= NewSession}, case State#state.type of none -> skip; protocol -> skip; protocol_local -> skip; _Type -> io:format(State#state.dumpfile,"EndClient:~w:~p~n",[ts_utils:time2sec_hires(When), Who]), io:format(State#state.dumpfile,"load:~w~n",[Clients]) end, {noreply, State#state{client = Clients, stats=NewStats}}; handle_cast({dumpstats}, State=#state{stats=Stats}) -> export_stats(State), NewSessions = ts_stats_mon:reset_all_stats(Stats#stats.session), NewOSmon = ts_stats_mon:reset_all_stats(Stats#stats.os_mon), NewStats = Stats#stats{session=NewSessions, os_mon=NewOSmon}, {noreply, State#state{laststats = Stats, stats=NewStats,lastdate=?NOW}}; handle_cast({sendmsg, _, _, _}, State = #state{type = none}) -> {noreply, State}; handle_cast({sendmsg, Who, When, What}, State = #state{type=light,dumpfile=Log}) -> io:format(Log,"Send:~w:~w:~-44s~n",[ts_utils:time2sec_hires(When),Who, binary_to_list(What)]), {noreply, State}; handle_cast({sendmsg, Who, When, What}, State=#state{type=full,dumpfile=Log}) when is_binary(What)-> io:format(Log,"Send:~w:~w:~s~n",[ts_utils:time2sec_hires(When),Who,binary_to_list(What)]), {noreply, State}; handle_cast({sendmsg, Who, When, What}, State=#state{type=full,dumpfile=Log}) -> io:format(Log,"Send:~w:~w:~p~n",[ts_utils:time2sec_hires(When),Who,What]), {noreply, State}; handle_cast({dump, Who, When, What}, State=#state{type=protocol,dumpfile=Log}) -> io:format(Log,"~w;~w;~s~n",[ts_utils:time2sec_hires(When),Who,What]), {noreply, State}; handle_cast({dump, cached, Data}, State=#state{type=protocol,dumpfile=Log}) -> file:write(Log,Data), {noreply, State}; handle_cast({rcvmsg, _, _, _}, State = #state{type=none}) -> {noreply, State}; handle_cast({rcvmsg, Who, When, What}, State = #state{type=light, dumpfile=Log}) when is_binary(What)-> io:format(Log,"Recv:~w:~w:~-44s~n",[ts_utils:time2sec_hires(When),Who, binary_to_list(What)]), {noreply, State}; handle_cast({rcvmsg, Who, When, What}, State = #state{type=light, dumpfile=Log}) -> io:format(Log,"Recv:~w:~w:~-44p~n",[ts_utils:time2sec_hires(When),Who, What]), {noreply, State}; handle_cast({rcvmsg, Who, When, What}, State=#state{type=full,dumpfile=Log}) when is_binary(What)-> io:format(Log, "Recv:~w:~w:~s~n",[ts_utils:time2sec_hires(When),Who,binary_to_list(What)]), {noreply, State}; handle_cast({rcvmsg, Who, When, What}, State=#state{type=full,dumpfile=Log}) -> io:format(Log, "Recv:~w:~w:~p~n",[ts_utils:time2sec_hires(When),Who,What]), {noreply, State}; handle_cast({stop}, State = #state{client=0, launchers=1, timer_ref=TRef}) -> ?LOG("Stop asked, no more users, last launcher stopped, OK to stop~n", ?INFO), case ?config(keep_web_gui) of true -> io:format(standard_io,"All slaves have stopped; keep controller and web dashboard alive. ~nHit CTRL-C or click Stop on the dashboard to stop.~n",[]), timer:cancel(TRef), close_stats(State), {noreply, State#state{wait_gui=true}}; _ -> {stop, normal, State} end; handle_cast({stop}, State=#state{launchers=L}) -> % we should stop, wait until no more clients are alive ?LOG("A launcher has finished, but not all users have finished, wait before stopping~n", ?NOTICE), {noreply, State#state{stop = true, launchers=L-1}}; handle_cast({launcher_is_alive}, State=#state{launchers=L}) -> ?LOG("A launcher has started~n", ?INFO), {noreply, State#state{launchers=L+1}}; handle_cast({abort}, State) -> % stop now ! ?LOG("Aborting by request !~n", ?EMERG), ts_stats_mon:add({ count, error_abort }), {stop, abort, State}; handle_cast(Msg, State) -> ?LOGF("Unknown msg ~p !~n",[Msg], ?WARN), {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. close_stats(State) -> export_stats(State), % blocking call to all ts_stats_mon; this way, we are % sure the last call to dumpstats is finished lists:foreach(fun(Name) -> ts_stats_mon:status(Name) end, ?STATSPROCS), case State#state.backend of json -> io:format(State#state.log,"]}]}~n",[]); _ -> io:format(State#state.log,"EndMonitor:~w~n",[?TIMESTAMP]) end, case State#state.log of standard_io -> ok; Dev -> file:close(Dev) end, file:close(State#state.fullstats). %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(Reason, #state{wait_gui=true}) -> ?LOGF("stopping monitor by gui (~p)~n",[Reason],?NOTICE); terminate(Reason, State) -> ?LOGF("stopping monitor (~p)~n",[Reason],?NOTICE), close_stats(State), slave:stop(node()). %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateData, _Extra) -> {ok, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: start_logger/3 %% Purpose: open log files and start timer %% Returns: {reply, ok, State} | {stop, Reason, State} %%---------------------------------------------------------------------- %% fulltext backend: open log file with compression enable and delayed_write start_logger({Machines, DumpType, fullstats}, From, State=#state{fullstats=undefined}) -> Filename = filename:join(State#state.log_dir,?FULLSTATS_FILENAME), ?LOG("Open file with delayed_write for fullstats backend~n",?NOTICE), case file:open(Filename,[write, {delayed_write, ?DELAYED_WRITE_SIZE, ?DELAYED_WRITE_DELAY}]) of {ok, Stream} -> start_logger({Machines, DumpType, fullstats}, From, State#state{fullstats=Stream}); {error, Reason} -> ?LOGF("Can't open mon log file ~p! ~p~n",[Filename,Reason], ?ERR), {stop, Reason, State} end; start_logger({Machines, DumpType, Backend}, _From, State=#state{log=Log,fullstats=FS}) -> ?LOGF("Activate clients with ~p backend~n",[Backend],?NOTICE), print_headline(Log,Backend), start_launchers(Machines), {ok, TRef} = timer:apply_interval(State#state.dump_interval, ?MODULE, dumpstats, [] ), lists:foreach(fun(Name) -> ts_stats_mon:set_output(Backend,{Log,FS}, Name) end, ?STATSPROCS), start_dump(State#state{type=DumpType, backend=Backend, timer_ref=TRef}). print_headline(Log,json)-> DateStr = ts_utils:now_sec(), io:format(Log,"{~n \"stats\": [~n {\"timestamp\": ~p, \"samples\": [",[DateStr]); print_headline(_Log,_Backend)-> ok. %% @spec start_dump(State::record(state)) -> {reply, Reply, State} %% @doc open file for dumping traffic start_dump(State=#state{type=none}) -> {reply, ok, State}; start_dump(State=#state{type=Type}) -> Filename = filename:join(State#state.log_dir,?DUMP_FILENAME), case file:open(Filename,[write, {delayed_write, ?DELAYED_WRITE_SIZE, ?DELAYED_WRITE_DELAY}]) of {ok, Stream} -> ?LOG("dump file opened, starting monitor~n",?INFO), case Type of protocol -> io:format(Stream,"#date;pid;id;http method;host;URL;HTTP status;size;duration;transaction;match;error;tag~n",[]); _ -> ok end, {reply, ok, State#state{dumpfile=Stream}}; {error, Reason} -> ?LOGF("Can't open mon dump file! ~p~n",[Reason], ?ERR), {reply, ok, State#state{type=none}} end. %%---------------------------------------------------------------------- %% Func: export_stats/1 %%---------------------------------------------------------------------- export_stats(State=#state{log=Log,stats=Stats,laststats=LastStats, backend=json}) -> DateStr = ts_utils:now_sec(), io:format(Log,"]},~n {\"timestamp\": ~w, \"samples\": [",[DateStr]), %% print number of simultaneous users io:format(Log," {\"name\": \"users\", \"value\": ~p, \"max\": ~p}",[State#state.client,State#state.maxclient]), export_stats_common(json, Stats,LastStats,Log); export_stats(State=#state{log=Log,stats=Stats,laststats=LastStats, backend=BackEnd}) -> DateStr = ts_utils:now_sec(), io:format(Log,"# stats: dump at ~w~n",[DateStr]), %% print number of simultaneous users io:format(Log,"stats: ~p ~p ~p~n",[users,State#state.client,State#state.maxclient]), export_stats_common(BackEnd, Stats,LastStats,Log). export_stats_common(BackEnd, Stats,LastStats,Log)-> Param = {BackEnd,LastStats#stats.os_mon,Log}, dict:fold(fun ts_stats_mon:print_stats/3, Param, Stats#stats.os_mon), ts_stats_mon:print_stats({session, sample}, Stats#stats.session,{BackEnd,[],Log}), ts_stats_mon:print_stats({users_count, count}, Stats#stats.users_count, {BackEnd,LastStats#stats.users_count,Log}), ts_stats_mon:print_stats({finish_users_count, count}, Stats#stats.finish_users_count, {BackEnd,LastStats#stats.finish_users_count,Log}), lists:foreach(fun(Name) -> ts_stats_mon:dumpstats(Name) end, ?STATSPROCS). %%---------------------------------------------------------------------- %% Func: start_launchers/2 %% @doc start the launcher on clients nodes %%---------------------------------------------------------------------- start_launchers(Machines) -> ?LOGF("Tsung clients setup: ~p~n",[Machines],?DEB), GetHost = fun(A) -> list_to_atom(A#client.host) end, HostList = lists:map(GetHost, Machines), ?LOGF("Starting tsung clients on hosts: ~p~n",[HostList],?NOTICE), %% starts beam on all client hosts ts_config_server:newbeams(HostList). %% post_process_logs(FileName) -> %% {ok, Device} = file:open(FileName, [read]), %% post_process_line(io:get_line(Device, ""), Device, []). %% post_process_line(eof, Device, State) -> %% file:close(Device); %% post_process_line("End "++ _, Device, Logs) -> %% post_process_line(io:get_line(Device, ""),Device, Logs); %% post_process_line("# stats: dump at "++ TimeStamp, D, Logs=#logs{start_time=undefined}) -> %% {StartTime,_}=string:to_integer(TimeStamp), %% post_process_line(io:get_line(D, ""),D, #logs{start_time=StartTime}); %% post_process_line("# stats: dump at "++ TimeStamp, Dev, Logs) -> %% {Time,_}=string:to_integer(TimeStamp), %% Current=Time-Logs#logs.start_time, %% post_process_line(io:get_line(Dev, ""),Dev, Logs#logs{current_time=Current}); %% post_process_line("# stats: "++ Stats, Dev, Logs) -> %% case string:tokens(Stats," ") of %% {"users",Count, GlobalCount} -> %% todo; %% {Name, Count, Max} -> %% todo; %% {"tr_" ++ TrName, Count, Mean, StdDev, Max, Min, GMean,GCount} -> %% todo; %% {"{"++ Name, Count, Mean, StdDev, Max, Min, GMean,GCount} -> %% todo; %% {Name, Count, Mean, StdDev, Max, Min, GMean,GCount} -> %% todo %% end, %% post_process_line(io:get_line(Dev, ""),Dev, Logs). tsung-1.8.0/src/tsung_controller/ts_match_logger.erl0000644000201100017670000001665214377756736022447 0ustar nniclausdream%%% %%% Copyright (C) 2008 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%---------------------------------------------------------------------- %% @copyright 2008 Nicolas Niclausse %% @author Nicolas Niclausse %% @since 1.3.1 , 19 Nov 2008 %% @doc log match entries @end %% ---------------------------------------------------------------------- -module(ts_match_logger). -author('nicolas@niclux.org'). -vc('$Id: ts_mon.erl 774 2007-11-20 09:36:13Z nniclausse $ '). -behaviour(gen_server). -include("ts_config.hrl"). -define(DELAYED_WRITE_SIZE,524288). % 512KB -define(DELAYED_WRITE_DELAY,5000). % 5 sec %% External exports, API -export([start/1, stop/0, add/1 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {filename, % log filename level, % type of backend: text|rrdtool|fullstats dumpid=1, % current dump id logdir, fd % file descriptor }). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% @spec start(LogDir::string()) -> ok | throw({error, Reason}) %% @doc Start the monitoring process %% @end %%---------------------------------------------------------------------- start(LogDir) -> ?LOG("starting match logger, global ~n",?INFO), gen_server:start_link({global, ?MODULE}, ?MODULE, [LogDir], []). stop() -> gen_server:cast({global, ?MODULE}, {stop}). %%---------------------------------------------------------------------- %% @spec add(Data::list()| {UserId::integer(),SessionId::integer(), %% RequestId::integer(),TimeStamp::tuple(),{count, Val::atom()}}) -> ok %% @doc log match entries %% @end %%---------------------------------------------------------------------- add(Data) -> gen_server:cast({global, ?MODULE}, {add, Data}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init([LogDir]) -> ?LOG("starting match logger~n",?INFO), Base = filename:basename(?config(match_log_file)), Filename = filename:join(LogDir, Base), case file:open(Filename,[write, {delayed_write, ?DELAYED_WRITE_SIZE, ?DELAYED_WRITE_DELAY}]) of {ok, Fd} -> ?LOG("starting match logger~n",?INFO), io:format(Fd,"# timestamp userid sessionid requestid event transaction name~n",[]), {ok, #state{ fd = Fd, filename = Filename, logdir = LogDir }}; {error, Reason} -> ?LOGF("Can't open match log file! ~p~n",[Reason], ?ERR), {stop, Reason} end. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call(Request, _From, State) -> ?LOGF("Unknown call ~p !~n",[Request],?ERR), Reply = ok, {reply, Reply, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast({add, List}, State) when is_list(List)-> NewState=lists:foldr(fun(X,Acc)-> log(X,Acc) end,State, List), {noreply,NewState}; handle_cast({add, Data}, State) when is_tuple(Data)-> NewState=log(Data,State), {noreply,NewState}; handle_cast({stop}, State) -> {stop, normal, State}; handle_cast(Msg, State) -> ?LOGF("Unknown msg ~p !~n",[Msg], ?WARN), {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(Reason, State) -> ?LOGF("stopping match logger (~p)~n",[Reason],?INFO), file:close(State#state.fd), ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateData, _Extra) -> {ok, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- log({UserId,SessionId,RequestId,TimeStamp,{count, Val},[], Tr,Name},State=#state{fd=File}) -> TS=ts_utils:time2sec_hires(TimeStamp), io:format(File,"~f ~B ~B ~B ~p ~s ~s~n",[TS,UserId,SessionId,RequestId,Val,ts_utils:log_transaction(Tr),Name]), State; log({UserId,SessionId,RequestId,TimeStamp,{count, Val},Bin, Tr,MatchName}, State=#state{logdir=LogDir, dumpid=Id}) -> log({UserId,SessionId,RequestId,TimeStamp,{count, Val},[],Tr, MatchName}, State), Name=ts_utils:join("-",lists:map(fun integer_to_list/1,[UserId,SessionId,RequestId,Id])), Filename=filename:join(LogDir, "match-"++ Name ++".dump"), file:write_file(Filename,Bin), State#state{dumpid=Id+1}. tsung-1.8.0/src/tsung_controller/ts_local_file_server.erl0000644000201100017670000001360414377756736023465 0ustar nniclausdream%%% Copyright (C) 2017 Sebastian Cohnen %%%------------------------------------------------------------------- %%% File : ts_local_file_server.erl %%% Author : Sebastian Cohnen %%% Description : Read a local, line-based file %%%------------------------------------------------------------------- -module(ts_local_file_server). -author('sebastian.cohnen@gmail.com'). -behaviour(gen_server). -export([ start_local/1, distribute_files/2, start/1, get_random_line/1, stop/1 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -include("ts_config.hrl"). -record(file, { items, %% tuple of lines read from file size %% total number of lines }). % Timeout in milliseconds for distributing each file to every subset % of Nodes (see DISTRIBUTION_MAX_CONCURRENCY). -define(RPC_TIMEOUT, 120000). % 2 minutes % Number of nodes that will be send files to concurrently. -define(DISTRIBUTION_MAX_CONCURRENCY, 10). %%%--------------------------------------------------------------------- %%% Public API %%%--------------------------------------------------------------------- start_local(FilePaths) when is_list(FilePaths) -> lists:foreach(fun({FileId, FilePath}) -> {ok, Lines} = read_file(FilePath), ok, _Pid = start({FileId, Lines}) end, FilePaths). distribute_files(Nodes, FilePaths) when is_list(FilePaths) -> lists:foreach(fun({FileId, FilePath}) -> {ok, Lines} = read_file(FilePath), ok = distribute_file_in_batches(Nodes, {FileId, Lines}) end, FilePaths). distribute_file_in_batches(Nodes, File) when length(Nodes) > ?DISTRIBUTION_MAX_CONCURRENCY -> {CurrentNodes, RestNodes} = lists:split(?DISTRIBUTION_MAX_CONCURRENCY, Nodes), ok = send_file_to_nodes(CurrentNodes, File), ok = distribute_file_in_batches(RestNodes, File); distribute_file_in_batches(Nodes, File) -> ok = send_file_to_nodes(Nodes, File). send_file_to_nodes(Nodes, {FileId, Lines}) -> {Responses, BadNodes} = rpc:multicall(Nodes, ?MODULE, start, [{FileId, Lines}], ?RPC_TIMEOUT), ?LOGF("Local File Server: RPC Result ~p ~n", [Responses], ?DEB), case BadNodes of [] -> % check that all resported back okay true = lists:all(fun({ok, _}) -> true end, Responses), ok; _ -> ?LOGF("Can't distribute data for ~p for local file servers to all nodes ~p~n", [FileId, BadNodes], ?ERR), {error, rpc_error} end. start({FileId, Lines}) -> gen_server:start({local, get_server_name(FileId)}, ?MODULE, Lines, []). stop(FileId) -> gen_server:call(get_server_name(FileId), stop). get_random_line(FileId)-> gen_server:call(get_server_name(FileId), {get_random_line}). %%%--------------------------------------------------------------------- %%% Callback functions from gen_server %%%--------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init(Lines) -> {ok, Lines}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call({get_random_line}, _From, State) -> I = random:uniform(State#file.size), Reply = {ok, element(I, State#file.items)}, {reply, Reply, State}; handle_call(stop, _From, State)-> {stop, normal, ok, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%--------------------------------------------------------------------- %%% Private %%%--------------------------------------------------------------------- get_server_name(FileId) -> list_to_atom(?MODULE_STRING ++ "_" ++ atom_to_list(FileId)). read_file(Path) -> case file:read_file(Path) of {ok, Bin} -> Lines = binary:split(Bin, <<"\n">>, [global, trim]), {ok, #file{items = list_to_tuple(Lines), size = length(Lines)}}; {error, Reason} -> ?LOGF("Local File Server: Error while opening file ~p :~p~n", [Path, Reason], ?ERR), {error, Reason} end. tsung-1.8.0/src/tsung_controller/ts_job_notify.erl0000644000201100017670000003104514377756736022147 0ustar nniclausdream%%% %%% Copyright 2011 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 04 mai 2011 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% %%% @doc %%% %%% @end -module(ts_job_notify). -vc('$Id: ts_notify.erl,v 0.0 2011/05/04 11:18:48 nniclaus Exp $ '). -author('nicolas.niclausse@inria.fr'). -behaviour(gen_server). -include("ts_macros.hrl"). -include("ts_job.hrl"). %% API -export([start_link/0]). -export([listen/1, monitor/1, demonitor/1, wait_jobs/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -record(state, {port, % listen port acceptsock, % The socket we are accept()ing at acceptloop_pid, % The PID of the companion process that blocks jobs}). %%%=================================================================== %%% API %%%=================================================================== %%-------------------------------------------------------------------- %% @doc %% Starts the server %% %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} %% @end %%-------------------------------------------------------------------- start_link() -> gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). listen(Port) -> gen_server:cast({global, ?MODULE}, {listen, Port}). monitor({JobID, OwnerPid, StartTime, QueuedTime, Dump}) -> gen_server:cast({global, ?MODULE}, {monitor, {JobID, OwnerPid, StartTime, QueuedTime,Dump}}). demonitor({JobID}) -> gen_server:cast({global, ?MODULE}, {monitor, {JobID}}). wait_jobs(Pid) -> gen_server:cast({global, ?MODULE}, {wait_jobs, Pid}). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %%-------------------------------------------------------------------- %% @private %% @doc %% Initializes the server %% %% @spec init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% @end %%-------------------------------------------------------------------- init([]) -> ?LOG("Starting~n",?INFO), case global:whereis_name(ts_config_server) of undefined -> {ok, #state{jobs=ets:new(jobs,[{keypos, #job_session.jobid}])}}; _Pid -> ?LOG("Config server is alive !~n",?INFO), case ts_config_server:get_jobs_state() of {Jobs,Port} -> ?LOG("Got backup of node state~n",?DEB), {noreply,NewState} = handle_cast({listen,Port}, #state{jobs=Jobs,port=Port}), {ok, NewState}; Else -> ?LOGF("Got this from config server:~p~n",[Else],?DEB), {ok, #state{jobs=ets:new(jobs,[{keypos, #job_session.jobid}])}} end end. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling call messages %% %% @spec handle_call(Request, From, State) -> %% {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_call({accepted, _Tag, Sock}, _From, State) -> ?LOGF("New socket:~p~n", [Sock],?DEB), {reply, continue, State#state{}}; handle_call({accept_error, _Tag, Error}, _From, State) -> ?LOGF("accept() failed ~p~n",[Error],?ERR), {stop, Error, stop, State}; handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling cast messages %% %% @spec handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_cast({monitor, {JobID, OwnerPid, SubmitTS, QueuedTS,Dump}}, State=#state{jobs=Jobs}) -> ?LOGF("monitoring job ~p from pid ~p~n",[JobID,OwnerPid],?DEB), ets:insert(Jobs,#job_session{jobid=JobID,owner=OwnerPid, submission_time=SubmitTS, queue_time=QueuedTS,dump=Dump}), SubmitTime=ts_utils:elapsed(SubmitTS,QueuedTS), ts_mon_cache:add([{sum,job_queued,1},{sample,tr_job_submit,SubmitTime}]), {noreply, State}; handle_cast({demonitor, {JobID}}, State=#state{jobs=Jobs}) -> ets:delete(Jobs,JobID), {noreply, State}; handle_cast({wait_jobs, Pid}, State=#state{jobs=Jobs}) -> %% look for all jobs started by this pid ?LOGF("look for job of ~p~n",[Pid],?DEB), check_jobs(Jobs,Pid), {noreply, State}; handle_cast({listen, undefined}, State) -> ?LOG("No listen port defined, can't open listening socket (don't worry: it's normal if you don't use job notifications) ~n",?INFO), {noreply, State}; handle_cast({listen,Port}, State) -> Opts = [{reuseaddr, true}, {active, once}], case gen_tcp:listen(Port, Opts) of {ok, ListenSock} -> ?LOGF("Listening on port ~p done, start accepting loop~n",[Port],?INFO), {noreply, State#state {acceptsock=ListenSock, port=Port, acceptloop_pid = spawn_link(ts_utils, accept_loop, [self(), unused, ListenSock])}}; {error, Reason} -> ?LOGF("Error when trying to listen to socket: ~p~n",[Reason],?ERR), {noreply, State} end; handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling all non call/cast messages %% %% @spec handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_info({tcp, Socket, Data}, State=#state{jobs=Jobs}) -> %% OAR: %% args are job_id,job_name,TAG,comment %% TAG can be: %% - RUNNING : when the job is launched %% - END : when the job is finished normally %% - ERROR : when the job is finished abnormally %% - INFO : used when oardel is called on the job %% - SUSPENDED : when the job is suspended %% - RESUMING : when the job is resumed ?LOGF("received ~p from socket ~p",[Data,Socket],?DEB), case string:tokens(Data," ") of [Id, _Name, "RUNNING"|_] -> ?LOGF("look for job ~p in table",[Id],?DEB), case ets:lookup(Jobs,Id) of [] -> ?LOGF("Job owner of ~p is unknown",[Id],?NOTICE); [Job] -> Now=?NOW, Queued=ts_utils:elapsed(Job#job_session.queue_time,Now), ts_mon_cache:add([{sample,tr_job_wait,Queued},{sum,job_running,1}, {sum,job_queued,-1}]), ets:update_element(Jobs,Id,{#job_session.start_time,Now}) end; [Id, Name, "END"|_] -> case ets:lookup(Jobs,Id) of [] -> ?LOGF("Job owner of ~p is unknown",[Id],?NOTICE); [Job=#job_session{start_time=undefined}] -> ?LOGF("ERROR: Start time of job ~p is unknown",[Id],?ERR), ts_mon_cache:add([{sum,job_running,-1}, {sum,ok_job ,1}]), ets:delete_object(Jobs,Job), check_jobs(Jobs,Job#job_session.owner); [Job]-> Now=?NOW, Duration=ts_utils:elapsed(Job#job_session.start_time,Now), ts_mon_cache:add([{sample,tr_job_duration,Duration},{sum,job_running,-1}, {sum,ok_job ,1}]), ts_job:dump(Job#job_session.dump,{none,Job#job_session{end_time=Now,status="ok"},Name}), ets:delete_object(Jobs,Job), check_jobs(Jobs,Job#job_session.owner) end; [Id, Name, "ERROR"|_] -> case ets:lookup(Jobs,Id) of [] -> ?LOGF("Job owner of ~p is unknown",[Id],?NOTICE); [Job=#job_session{start_time=undefined}] -> ?LOGF("ERROR: start time of job ~p is unknown",[Id],?ERR), ts_mon_cache:add([{sum,job_running,-1}, {sum,error_job,1}]), ets:delete_object(Jobs,Job), check_jobs(Jobs,Job#job_session.owner); [Job]-> Now=?NOW, Duration=ts_utils:elapsed(Job#job_session.start_time,Now), ts_mon_cache:add([{sample,tr_job_duration,Duration},{sum,job_running,-1}, {sum,error_job,1}]), ts_job:dump(Job#job_session.dump,{none,Job#job_session{end_time=Now,status="error"},Name}), ets:delete_object(Jobs,Job), check_jobs(Jobs,Job#job_session.owner) end; [_Id, _Name, "INFO"|_] -> ok; [_Id, _Name, "SUSPENDED"|_] -> ok; [_Id, _Name, "RESUMING"|_] -> ok end, inet:setopts(Socket,[{active,once}]), {noreply, State}; handle_info({tcp_closed, _Socket}, State) -> {noreply, State}; handle_info({'ETS-TRANSFER',_Tab,_FromPid,_GiftData}, State=#state{}) -> ?LOG("Got ownership on job state table", ?NOTICE), {noreply, State}; handle_info(Info, State) -> ?LOGF("Unexpected message received: ~p", [Info], ?WARN), {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @spec terminate(Reason, State) -> void() %% @end %%-------------------------------------------------------------------- terminate(normal, _State) -> ?LOG("Terminating for normal reason", ?WARN), ok; terminate(Reason, State) when is_integer(State#state.port)-> ?LOGF("Terminating for reason ~p", [Reason], ?WARN), Pid=global:whereis_name(ts_config_server), ?LOGF("Config server pid is ~p", [Pid], ?DEB), ets:give_away(State#state.jobs,Pid,State#state.port), ok; terminate(Reason, State) -> ?LOGF("Terminating for reason ~p ~p", [Reason,State], ?WARN), ok. %%-------------------------------------------------------------------- %% @private %% @doc %% Convert process state when code is changed %% %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} %% @end %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== check_jobs(Jobs,Pid)-> case ets:match_object(Jobs, #job_session{owner=Pid, _='_'}) of [] -> ?LOGF("no jobs for pid ~p~n",[Pid],?DEB), Pid ! {erlang, ok, nojobs}; PidJobs-> ?LOGF("still ~p jobs for pid ~p~n",[length(PidJobs),Pid],?INFO) end. tsung-1.8.0/src/tsung_controller/ts_interaction_server.erl0000644000201100017670000002055414377756736023715 0ustar nniclausdream%%%------------------------------------------------------------------- %%% @author Nicolas Niclausse %%% @copyright (C) 2012, Nicolas Niclausse %%% @doc %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% %%% @end %%% Created: 20 août 2009 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_interaction_server). -behaviour(gen_server). -include("ts_macros.hrl"). %% API -export([start/0, send/1, rcv/1, notify/1, delete/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -record(state, {to, notify}). %%%=================================================================== %%% API %%%=================================================================== %%-------------------------------------------------------------------- %% @doc %% Starts the server %% %% @spec start() -> {ok, Pid} | ignore | {error, Error} %% @end %%-------------------------------------------------------------------- start() -> gen_server:start_link({global, ?SERVER}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% @spec send({StatsName::atom(), Date::term()}) -> ok %% @doc %% %% %% @end %%-------------------------------------------------------------------- send({StatsName, Date}) when is_atom(StatsName) -> gen_server:cast({global, ?SERVER}, {send, StatsName, Date}). %%-------------------------------------------------------------------- %% @spec rcv({StatsName, Date}) -> ok %% @doc %% %% @end %%-------------------------------------------------------------------- rcv({StatsName, Date}) when is_atom(StatsName) -> gen_server:cast({global, ?SERVER}, {'receive', StatsName, Date}). %%-------------------------------------------------------------------- %% @spec delete({StatsName}) -> ok %% @doc %% %% %% @end %%-------------------------------------------------------------------- delete({StatsName}) when is_atom(StatsName) -> gen_server:cast({global, ?SERVER}, {delete, StatsName}). %%-------------------------------------------------------------------- %% @spec notify({Action::atom(), StatsName::atom(), Pid::pid()}) -> ok %% @doc %% %% %% @end %%-------------------------------------------------------------------- notify({Action, StatsName, Pid}) when is_atom(Action), is_atom(StatsName), is_pid(Pid) -> gen_server:cast({global, ?SERVER}, {notify,Action,StatsName,Pid}). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %%-------------------------------------------------------------------- %% @private %% @doc %% Initializes the server %% %% @spec init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% @end %%-------------------------------------------------------------------- init([]) -> {ok, #state{to=ets:new(to, []), notify=ets:new(notify, [bag])}}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling call messages %% %% @spec handle_call(Request, From, State) -> %% {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling cast messages %% %% @spec handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_cast({send, StatsName, Date}, State=#state{to=To, notify=Notify}) -> case ets:lookup(To,StatsName) of [] -> ?LOGF("interaction: setting send timestamp for ~p~n",[StatsName],?DEB); _Val -> ?LOGF("interaction: resetting send timestamp for ~p~n",[StatsName],?WARN) end, ets:insert(To,{StatsName, Date}), handle_notification(Notify,{send, StatsName}), {noreply, State}; handle_cast({'receive',StatsName, EndDate}, State=#state{to=To, notify=Notify}) -> ?LOGF("~p ~n",[StatsName],?DEB), case ets:lookup(To,StatsName) of [] -> handle_notification(Notify,{'receive',StatsName}), {noreply, State}; [{_Key, StartDate}] -> ?LOGF("to/from ended, logging ~p ~n",[StatsName],?DEB), handle_notification(Notify,{'receive',StatsName}), ts_mon_cache:add({sample,StatsName, ts_utils:elapsed(StartDate,EndDate)}), {noreply, State} end; handle_cast({delete, StatsName}, State=#state{to=To}) -> ets:delete(To,StatsName), {noreply, State}; handle_cast({notify, Action, StatsName, Pid}, State=#state{notify=Notify}) -> %% TODO: check if event already exists ? ets:insert(Notify,{{StatsName, Action}, {Pid}}), {noreply, State}; handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling all non call/cast messages %% %% @spec handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @spec terminate(Reason, State) -> void() %% @end %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% @private %% @doc %% Convert process state when code is changed %% %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} %% @end %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== handle_notification(Notify, {Action, StatsName}) -> case ets:lookup(Notify, {StatsName, Action}) of [] -> ok; List -> ?LOGF("gotlist ~p~n",List,?DEB), Fun = fun({{Stats, Action2},{Pid}}) -> ?LOGF("sending msg to pid ~p~n",[Pid],?DEB), Pid ! {notify, Action2, Stats} end, lists:foreach(Fun,List), ets:delete(Notify,StatsName) end. tsung-1.8.0/src/tsung_controller/ts_file_server.erl0000644000201100017670000002061314377756736022311 0ustar nniclausdream%%% Copyright (C) 2005 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_file_server.erl %%% Author : Nicolas Niclausse %%% Description : Read a line-based file %%% %%% Created : 6 Jul 2005 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_file_server). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). %% External exports -export([start/0, get_random_line/0, get_random_line/1, get_next_line/0, get_next_line/1, get_all_lines/0, get_all_lines/1, stop/0, read/1, read/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(file, {items, %% tuple of lines read from a file size, %% total number of lines current=-1 %% current line in file }). -record(state, {files}). -define(DICT, dict). -include("ts_config.hrl"). -include("xmerl.hrl"). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: Binary %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- read(Filenames) -> gen_server:call({global, ?MODULE}, {read, Filenames}, ?config(file_server_timeout)). read(Filenames, Timeout) -> gen_server:call({global, ?MODULE}, {read, Filenames}, Timeout). start() -> ?LOG("Starting~n",?DEB), gen_server:start_link({global, ?MODULE}, ?MODULE, [], []). get_random_line({Pid,_DynData}) when is_pid(Pid)-> %% called within a substitution (eg. file is 'default') case get_random_line(default) of {ok, Val} -> Val; Error -> Error end; get_random_line(FileID)-> gen_server:call({global, ?MODULE}, {get_random_line, FileID}). get_random_line() -> get_random_line(default). get_next_line({Pid,_DynData}) when is_pid(Pid)-> %% called within a substitution (eg. file is 'default') case get_next_line(default) of {ok, Val} -> Val; Error -> Error end; get_next_line(FileID)-> gen_server:call({global, ?MODULE}, {get_next_line, FileID}). get_next_line() -> get_next_line(default). get_all_lines({Pid,_DynData}) when is_pid(Pid)-> %% called within a substitution (eg. file is 'default') case get_all_lines(default) of {ok, Val} -> Val; Error -> Error end; get_all_lines(FileID)-> gen_server:call({global, ?MODULE}, {get_all_lines, FileID}). get_all_lines() -> get_all_lines(default). stop()-> gen_server:call({global, ?MODULE}, stop). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init([]) -> {ok, #state{files=?DICT:new()}}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call({get_all_lines, FileID}, _From, State) -> FileDesc = ?DICT:fetch(FileID, State#state.files), Reply = {ok, tuple_to_list(FileDesc#file.items)}, {reply, Reply, State}; handle_call({get_random_line, FileID}, _From, State) -> FileDesc = ?DICT:fetch(FileID, State#state.files), I = random:uniform(FileDesc#file.size), Reply = {ok, element(I, FileDesc#file.items)}, {reply, Reply, State}; handle_call({get_next_line, FileID}, _From, State) -> FileDesc = ?DICT:fetch(FileID, State#state.files), I = (FileDesc#file.current + 1) rem FileDesc#file.size, Reply = {ok, element(I+1, FileDesc#file.items)}, NewFileDesc = FileDesc#file{current=I}, {reply, Reply, State#state{files=?DICT:store(FileID, NewFileDesc, State#state.files)}}; handle_call({read, Filenames}, _From, State) -> lists:foldl(fun open_file/2, {reply, ok, State}, Filenames); handle_call(stop, _From, State)-> {stop, normal, ok, State}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Open a file and return a new state %%---------------------------------------------------------------------- open_file({ID, Path}, {reply, Result, State}) -> case ?DICT:find(ID, State#state.files) of {ok, _} -> ?LOGF("File with id ~p already opened (path is ~p)~n",[ID, Path], ?WARN), {reply, {error, already_open}, State}; error -> ?LOGF("Opening file ~p~n",[Path], ?INFO), case file:read_file(Path) of {ok, Bin} -> List_items = binary:split( Bin , <<"\n">> , [global,trim]), FileDesc = #file{items = list_to_tuple(List_items), size=length(List_items)}, {reply, Result, State#state{files = ?DICT:store(ID, FileDesc, State#state.files)}}; {error,Reason} -> ?LOGF("Error while opening file ~p :~p~n",[ Path, Reason], ?ERR), {reply, {error, Path}, State} end end. tsung-1.8.0/src/tsung_controller/ts_controller_sup.erl0000644000201100017670000001603114377756736023055 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2003 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_controller_sup). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -include("ts_macros.hrl"). -behaviour(supervisor). %% External exports -export([start_link/1, start_inets/2]). %% supervisor callbacks -export([init/1]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link(LogDir) -> ?LOG("starting supervisor ...~n",?INFO), supervisor:start_link({local, ?MODULE}, ?MODULE, [LogDir]). %%%---------------------------------------------------------------------- %%% Callback functions from supervisor %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, {SupFlags, [ChildSpec]}} | %% ignore | %% {error, Reason} %%---------------------------------------------------------------------- init([LogDir]) -> ?LOG("starting",?INFO), Config = {ts_config_server, {ts_config_server, start_link, [LogDir]}, transient, 2000, worker, [ts_config_server]}, Mon = {ts_mon, {ts_mon, start, [LogDir]}, transient, 2000, worker, [ts_mon]}, Stats_Mon = {ts_stats_mon, {ts_stats_mon, start, []}, transient, 2000, worker, [ts_stats_mon]}, Request_Mon = {request, {ts_stats_mon, start, [request]}, transient, 2000, worker, [ts_stats_mon]}, Page_Mon = {page, {ts_stats_mon, start, [page]}, transient, 2000, worker, [ts_stats_mon]}, Connect_Mon = {connect, {ts_stats_mon, start, [connect]}, transient, 2000, worker, [ts_stats_mon]}, Transaction_Mon = {transaction, {ts_stats_mon, start, [transaction]}, transient, 2000, worker, [ts_stats_mon]}, Match_Log = {ts_match_logger, {ts_match_logger, start, [LogDir]}, transient, 2000, worker, [ts_match_logger]}, ErlangSup = {ts_erlang_mon_sup, {ts_os_mon_sup, start_link, [erlang]}, permanent, 2000, supervisor, [ts_os_mon_sup]}, MuninSup = {ts_munin_mon_sup, {ts_os_mon_sup, start_link, [munin]}, permanent, 2000, supervisor, [ts_os_mon_sup]}, SNMPSup = {ts_snmp_mon_sup, {ts_os_mon_sup, start_link, [snmp]}, permanent, 2000, supervisor, [ts_os_mon_sup]}, Timer = {ts_timer, {ts_timer, start, [?config(nclients)]}, transient, 2000, worker, [ts_timer]}, Msg = {ts_msg_server, {ts_msg_server, start, []}, transient, 2000, worker, [ts_msg_server]}, UserSup = {ts_user_server_sup,{ts_user_server_sup,start_link,[]},transient,2000, supervisor,[ts_user_server_sup]}, Notify = {ts_job_notify, {ts_job_notify, start_link, []}, transient, 2000, worker, [ts_job_notify]}, Interaction = {ts_interaction_server, {ts_interaction_server, start, []}, transient, 2000, worker, [ts_interaction_server]}, case application:get_env(tsung_controller,web_gui) of {ok, true} -> Redirect= << "\n" >>, start_inets(LogDir, Redirect); _ -> ?LOG("Web gui disabled, skip inets",?INFO) end, {ok,{{one_for_one,?retries,10}, [Config, Mon, Stats_Mon, Request_Mon, Page_Mon, Connect_Mon, Transaction_Mon, Match_Log, Timer, Msg, Notify, Interaction, UserSup, ErlangSup, MuninSup,SNMPSup]}}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- start_inets(LogDir,Redirect) -> inets:start(), Path = filename:join(template_path(), "style"), {ok,Styles} = file:list_dir(Path), DestDir = filename:join(LogDir,"style"), file:make_dir(DestDir), lists:foreach(fun(CSS) -> DestName = filename:join(DestDir,CSS), file:copy(filename:join(Path,CSS),DestName) end,Styles), file:write_file(filename:join(LogDir,"index.html"), Redirect), case inets:start(httpd, [{port, 8091}, {modules,[mod_esi, mod_dir, mod_alias, mod_get, mod_head, mod_log, mod_disk_log]}, {erl_script_alias, {"/es", [ts_web, ts_api]}}, {error_log, "inets_error.log"}, %% {transfer_log, "inets_access.log"}, {directory_index, ["index.html"]}, {mime_types,[ {"html","text/html"}, {"css","text/css"}, {"png","image/png"}, {"xml","text/xml"}, {"json","application/json"}, {"js","application/x-javascript"}]}, {server_name,"tsung_controller"}, {server_root,LogDir}, {document_root,LogDir}]) of {ok, _Pid} -> ?LOG("Starting inets on port 8091",?INFO); Error -> ?LOGF("Error while starting inets on port 8091: ~p",[Error],?ERR) end. template_path() -> case ?config(template_path) of beam_relative -> filename:join(filename:dirname(code:which(tsung_controller)),"../../../../share/tsung/templates"); Other -> Other end. tsung-1.8.0/src/tsung_controller/ts_config_websocket.erl0000644000201100017670000000727214377756736023325 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_websocket). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_websocket.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name = dyn_variable}, Conf = #config{}) -> ts_config:parse(Element, Conf); parse_config(Element = #xmlElement{name = websocket}, Config = #config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar = DynVar, subst = SubstFlag, match = MatchRegExp}) -> Type = ts_config:getAttr(atom, Element#xmlElement.attributes, type), ValRaw = ts_config:getText(Element#xmlElement.content), Path = ts_config:getAttr(string, Element#xmlElement.attributes, path, "/"), Origin = ts_config:getAttr(string, Element#xmlElement.attributes, origin, ""), SubProtocols = ts_config:getAttr(string, Element#xmlElement.attributes, subprotocols, ""), Frame = ts_config:getAttr(string, Element#xmlElement.attributes, frame, "binary"), Headers = ts_config_http:parse_headers(Element#xmlElement.content, []), Request = #websocket_request{data = ValRaw, type = Type, subprotos = SubProtocols, origin = Origin, path = Path, frame = Frame, headers = Headers}, Ack = case Type of message -> ts_config:getAttr(atom, Element#xmlElement.attributes, ack, parse); _ -> parse end, Msg = #ts_request{ack = Ack, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab, {{CurS#session.id, Id}, Msg }), lists:foldl( fun(A, B)->ts_config:parse(A, B) end, Config#config{dynvar = []}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_shell.erl0000644000201100017670000000554414377756736022446 0ustar nniclausdream%%% %%% Copyright 2010 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 18 août 2010 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_shell). -vc('$Id$ '). -author('nicolas.niclausse@sophia.inria.fr'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). -include("ts_shell.hrl"). %% @spec parse_config(#xmlElement{}, Config::term()) -> NewConfig::term() %% @doc Parses a tsung.xml configuration file xml element for this %% protocol and updates the Config term. %% @end parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=shell}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Cmd = ts_config:getAttr(string,Element#xmlElement.attributes, cmd), Args = ts_config:getAttr(string,Element#xmlElement.attributes, args, ""), Request = #shell{command=Cmd,args=Args}, Msg= #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id},Msg}), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_server.erl0000644000201100017670000013535714377756736022653 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2003 %%% %%% Author : Nicolas Niclausse %%% Created: 04 Dec 2003 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_config_server.erl %%% Author : Nicolas Niclausse %%% Description : %%% %%% Created : 4 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_config_server). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- -include("ts_profile.hrl"). -include("ts_config.hrl"). %%-------------------------------------------------------------------- %% External exports -export([start_link/1, read_config/1, read_config/2, get_req/2, get_next_session/1, get_client_config/1, newbeams/1, newbeam/2, stop/0, get_monitor_hosts/0, encode_filename/1, decode_filename/1, endlaunching/1, status/0, start_file_server/1, get_user_agents/0, get_client_config/2, get_user_param/1, get_user_port/1, get_jobs_state/0 ]). %%debug -export([choose_client_ip/1, choose_session/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {config, logdir, curcfg = 0, % number of configured launchers client_static_users = 0, % number of clients that already have their static users static_users = 0, % static users not yet given to a client ports, % dict, used if we need to choose the client port users=1, % userid (incremental counter) start_date, % hostname, % controller hostname last_beam_id = 0, % last tsung beam id (used to set nodenames) ending_beams = 0, % number of beams with no new users to start lastips, % store next ip to choose for each client host total_weight % total weight of client machines }). -define(RPC_TIMEOUT, 30000). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link/0 %% Description: Starts the server %%-------------------------------------------------------------------- start_link(LogDir) -> gen_server:start_link({global, ?MODULE}, ?MODULE, [LogDir], []). stop() -> gen_server:cast({global, ?MODULE}, {abort}). status() -> gen_server:call({global, ?MODULE}, {status}). %%-------------------------------------------------------------------- %% Function: newbeam/1 %% Description: start a new beam %%-------------------------------------------------------------------- newbeams(HostList)-> gen_server:cast({global, ?MODULE},{newbeams, HostList }). %%-------------------------------------------------------------------- %% Function: newbeam/2 %% Description: start a new beam with given config. Use by launcher %% when maxclient is reached. In this case, the arrival rate is known %%-------------------------------------------------------------------- newbeam(Host, Args)-> gen_server:cast({global, ?MODULE},{newbeam, Host, Args }). %%-------------------------------------------------------------------- %% Function: get_req/2 %% Description: get Nth request from given session Id %% Returns: #message | {error, Reason} %%-------------------------------------------------------------------- get_req(Id, Count)-> gen_server:call({global, ?MODULE},{get_req, Id, Count}). %%-------------------------------------------------------------------- %% Function: get_user_agents/0 %% Description: %% Returns: List %%-------------------------------------------------------------------- get_user_agents()-> gen_server:call({global, ?MODULE},{get_user_agents}). %%-------------------------------------------------------------------- %% Function: read_config/1 %% Description: Read Config file %% Returns: ok | {error, Reason} %%-------------------------------------------------------------------- read_config(ConfigFile)-> read_config(ConfigFile,?config_timeout). read_config(ConfigFile,Timeout)-> gen_server:call({global,?MODULE},{read_config, ConfigFile},Timeout). %%-------------------------------------------------------------------- %% Function: get_client_config/1 %% Description: get client machine setup (for the launcher) %% Returns: {ok, {ArrivalList, StartDate, MaxUsers}} | {error, notfound} %%-------------------------------------------------------------------- get_client_config(Host)-> gen_server:call({global,?MODULE},{get_client_config, Host}, ?config_timeout). get_client_config(Type, Host)-> gen_server:call({global,?MODULE},{get_client_config, Type, Host}, ?config_timeout). %%-------------------------------------------------------------------- %% @spec get_monitor_hosts() -> List %% List = [Hosts::string()] %% @doc get list of hosts to monitor @end %%-------------------------------------------------------------------- get_monitor_hosts()-> gen_server:call({global,?MODULE},{get_monitor_hosts}). %%-------------------------------------------------------------------- %% @spec get_next_session({Host::string(), PhaseId::integer()}) -> %% {ok, SessionId::integer(), SessionSize::integer(), IP::tuple(), UserId::integer() } %% @doc Choose randomly a session %% @end %%-------------------------------------------------------------------- get_next_session({Host, PhaseId})-> gen_server:call({global, ?MODULE},{get_next_session, Host, PhaseId}). get_user_param(Host)-> gen_server:call({global, ?MODULE},{get_user_param, Host}). get_user_port(Ip) -> gen_server:call({global, ?MODULE},{get_user_port, Ip}). endlaunching(Node) -> gen_server:cast({global, ?MODULE},{end_launching, Node}). get_jobs_state() -> gen_server:call({global, ?MODULE},{get_jobs_state}). %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init([LogDir]) -> process_flag(trap_exit,true), {ok, MyHostName} = ts_utils:node_to_hostname(node()), ?LOGF("Config server started, logdir is ~p~n ",[LogDir],?NOTICE), {ok, #state{logdir=LogDir, hostname=list_to_atom(MyHostName)}}. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call({read_config, ConfigFile}, _From, State=#state{logdir=LogDir}) -> case catch ts_config:read(ConfigFile, LogDir) of {ok, Config=#config{curid=LastReqId,sessions=[LastSess| Sessions]}} -> ts_utils:init_seed(Config#config.seed), ts_user_server:init_seed(Config#config.seed), application:set_env(tsung_controller, clients, Config#config.clients), application:set_env(tsung_controller, dump, Config#config.dump), application:set_env(tsung_controller, stats_backend, Config#config.stats_backend), application:set_env(tsung_controller, debug_level, Config#config.loglevel), SumWeights = fun(X, Sum) -> X#client.weight + Sum end, Sum = lists:foldl(SumWeights, 0, Config#config.clients), %% we only know now the size of last session from the file: add it %% in the table print_info(), NewLast=LastSess#session{size = LastReqId, type=Config#config.main_sess_type}, %% start the file server (if defined) using a separate process (it can be long) spawn(?MODULE, start_file_server, [Config]), ConfigTmp = loop_load(sort_static(Config#config{sessions=[NewLast]++Sessions})), %% Compute per phase popularities NewConfig = compute_popularities(ConfigTmp), ts_job_notify:listen(NewConfig#config.job_notify_port), case check_config(NewConfig) of ok -> {reply, ok, State#state{config=NewConfig, static_users=NewConfig#config.static_users,total_weight = Sum}}; {error, Reason} -> ?LOGF("Error while checking config: ~p~n",[Reason],?EMERG), {reply, {error, Reason}, State} end; {error, {{case_clause, {error, enoent}}, [{xmerl_scan, fetch_DTD, 2,_}|_]}} -> ?LOG("Error while parsing XML: DTD not found !~n",?EMERG), {reply, {error, dtd_not_found}, State}; {error, Reason} -> ?LOGF("Error while parsing XML config file: ~p~n",[Reason],?EMERG), {reply, {error, Reason}, State}; {'EXIT', Reason} -> ?LOGF("Error while parsing XML config file: ~p~n",[Reason],?EMERG), {reply, {error, Reason}, State} end; %% get Nth request from given session Id handle_call({get_req, Id, N}, _From, State) -> Config = State#state.config, Tab = Config#config.session_tab, ?DebugF("look for ~p th request in session ~p for ~p~n",[N,Id,_From]), case ets:lookup(Tab, {Id, N}) of [{_, Session}] -> ?DebugF("ok, found ~p for ~p~n",[Session,_From]), {reply, Session, State}; Other -> {reply, {error, Other}, State} end; handle_call({get_user_agents}, _From, State) -> Config = State#state.config, case ets:lookup(Config#config.session_tab, {http_user_agent, value}) of [] -> {reply, empty, State}; [{_Key, UserAgents}] -> {reply, UserAgents, State} end; %% get user parameters (static user: the session id is already known) handle_call({get_user_param, HostName}, _From, State=#state{users=UserId}) -> Config = State#state.config, {value, Client} = lists:keysearch(HostName, #client.host, Config#config.clients), {IPParam, Server} = get_user_param(Client,Config), ts_mon:newclient({static,?TIMESTAMP}), {reply, {ok, { IPParam, Server, UserId,Config#config.dump,Config#config.seed}}, State#state{users=UserId+1}}; %% get user port. This is needed by bosh, as there are more than one socket per bosh connection. handle_call({get_user_port, IP}, _From, State=#state{ports=Ports}) -> Config = State#state.config, {NewPorts,CPort} = choose_port(IP, Ports,Config#config.ports_range), {reply, {ok, CPort}, State#state{ports = NewPorts}}; %% get a new session id and user parameters for the given node handle_call({get_next_session, HostName, PhaseId}, _From, State=#state{users=Users}) -> Config = State#state.config, {value, Client} = lists:keysearch(HostName, #client.host, Config#config.clients), ?DebugF("get new session for ~p~n",[_From]), case choose_session(Config#config.sessions, Config#config.total_popularity, PhaseId) of {ok, Session=#session{id=Id}} -> ?LOGF("Session ~p chosen~n",[Id],?INFO), ts_mon:newclient({Id,?TIMESTAMP}), {IPParam, Server} = get_user_param(Client,Config), {reply, {ok, Session#session{client_ip= IPParam, server=Server,userid=Users, dump=Config#config.dump, seed=Config#config.seed}}, State#state{users=Users+1} }; Other -> {reply, {error, Other}, State} end; handle_call({get_client_config, static, Host}, _From, State=#state{config=Config}) -> %% static users (eg. each user started once at fixed time) %% we must spread this list of fixed users to each beam %% If we have N users and M client beams Clients=Config#config.clients, StaticUsers=State#state.static_users, Done=State#state.client_static_users, % number of clients that already have their static users {value, Client} = lists:keysearch(Host, #client.host, Clients), StartDate = set_start_date(State#state.start_date), case Done +1 == length(Clients) of true -> % last client, give him all pending users {reply,{ok,StaticUsers,StartDate},State#state{start_date=StartDate,static_users=[]}}; false -> Weight = Client#client.weight, Number=ts_utils:ceiling(length(StaticUsers)*Weight/State#state.total_weight), {NewUsers,Tail}=lists:split(Number,StaticUsers), {reply,{ok,NewUsers,StartDate},State#state{client_static_users=Done+1,start_date=StartDate,static_users=Tail}} end; %% get randomly generated users handle_call({get_client_config, Host}, _From, State=#state{curcfg=OldCfg,total_weight=Total_Weight}) -> ?DebugF("get_client_config from ~p~n",[Host]), Config = State#state.config, Clients=Config#config.clients, %% set start date if not done yet StartDate = set_start_date(State#state.start_date), {value, Client} = lists:keysearch(Host, #client.host, Clients), IsLast = OldCfg + 1 >= length(Clients),% test if this is the last launcher to ask for it's config Get = fun(Phase,Args)-> {get_client_cfg(Phase,Args),Args} end, {Res, _Acc} = lists:mapfoldl(Get, {Total_Weight,Client,IsLast},Config#config.arrivalphases), {NewPhases,ClientParams} = lists:unzip(Res), Reply = {ok,{ClientParams,StartDate,Client#client.maxusers}}, NewConfig=Config#config{arrivalphases=NewPhases}, {reply,Reply,State#state{config=NewConfig,start_date=StartDate, curcfg = OldCfg +1}}; %% handle_call({get_monitor_hosts}, _From, State) -> Config = State#state.config, {reply, Config#config.monitor_hosts, State}; % get status: send the number of actives nodes, number of phases handle_call({status}, _From, State) -> Config = State#state.config, Reply = {ok, length(Config#config.clients), State#state.ending_beams, length(Config#config.arrivalphases) }, {reply, Reply, State}; handle_call({get_jobs_state}, _From, State) when State#state.config == undefined -> {reply, not_configured, State}; handle_call({get_jobs_state}, {Pid,_Tag}, State) -> Config = State#state.config, Reply = case Config#config.job_notify_port of {Ets,Port} -> ets:give_away(Ets,Pid,Port), {Ets,Port}; Else -> Else end, {reply, Reply, State}; handle_call(Request, _From, State) -> ?LOGF("Unknown call ~p !~n",[Request],?ERR), {reply, ok, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- %% start the launcher on the current beam handle_cast({newbeams, HostList}, State=#state{logdir = LogDir, hostname = LocalHost, config = Config}) -> LocalVM = Config#config.use_controller_vm, GetLocal = fun(Host)-> is_vm_local(Host,LocalHost,LocalVM) end, {LocalBeams, RemoteBeams} = lists:partition(GetLocal,HostList), case { local_launcher(LocalBeams, LogDir, Config), RemoteBeams} of { {error, _Reason}, _ } -> ts_mon:abort(), {stop, normal,State}; {Id0, [] } -> % no remote beams set_max_duration(Config#config.duration), % local file servers if required case length(Config#config.local_file_server) of 0 -> ?LOG("Local file servers: None defined~n", ?NOTICE), ok; _ -> case ts_local_file_server:start_local(Config#config.local_file_server) of ok -> ?LOG("Local file servers: setup complete ~n", ?NOTICE); {error, Reason} -> ?LOGF("Local file servers: ~p~n", [Reason], ?ERR), ts_mon:abort(), exit({error, Reason}) end end, {noreply, State#state{last_beam_id = Id0}}; {Id0, _ } -> Seed = Config#config.seed, Args = set_remote_args(LogDir, Config#config.ports_range), Packed=ts_utils:pack(RemoteBeams), NNodes=length(Packed), MaxStartup = 9, % not 10 because os mon can also start a ssh connection MinBeamPerNode = min(MaxStartup, lists:min(lists:map(fun(A)->length(A) end, Packed))), SpreadedBeams = spread_nodelist(RemoteBeams), {BeamsIds, LastId} = lists:mapfoldl(fun(A,Acc) -> {{A, Acc}, Acc+1} end, Id0, SpreadedBeams), Fun = fun({Host,Id}) -> remote_launcher(Host, Id, Args) end, %% start beams in parallel, at most 10 in parallel per node %% (because sshd MaxSessions = 10, in case we have to %% start more than 10 beams on a single host) MaxParalRemote= NNodes * MinBeamPerNode + MaxStartup - MinBeamPerNode, %% now try to not overload the controller: MaxLaunchPerCore = Config#config.max_ssh_startup, Ncores = case erlang:system_info(logical_processors_available) of unknown -> erlang:system_info(logical_processors); N -> N end, MaxParal = min(Ncores * MaxLaunchPerCore, MaxParalRemote), ?LOGF("Try to start at most ~p remote nodes in parallel (cores: ~p, Max remote: ~p)", [MaxParal, Ncores, MaxParalRemote], ?INFO), RemoteNodes = ts_utils:pmap(Fun, BeamsIds, MaxParal), check_remotes_ok(RemoteNodes), ?LOG("All remote beams started, syncing ~n",?NOTICE), global:sync(), ?LOG("Syncing done, start remote tsung application ~n", ?INFO), % Distributing files for local file servers if required case length(Config#config.local_file_server) of 0 -> ?LOG("Local file servers: None defined~n", ?NOTICE), ok; _ -> case ts_local_file_server:distribute_files(RemoteNodes, Config#config.local_file_server) of ok -> ?LOG("Local file servers: setup complete ~n", ?NOTICE); {error, Reason} -> ?LOGF("Local file servers: ~p~n", [Reason], ?ERR), ts_mon:abort(), exit({error, Reason}) end end, ?LOG("Start remote tsung application ~n", ?DEB), {Resl, BadNodes} = rpc:multicall(RemoteNodes,tsung,start,[],?RPC_TIMEOUT), ?LOGF("RPC result: ~p ~p ~n",[Resl,BadNodes],?DEB), case BadNodes of [] -> StartLaunchers = fun(Node) -> ts_launcher_static:launch({Node,[]}), ts_launcher:launch({Node, [], Seed}) end, case Config#config.ports_range of undefined -> ?LOG("Undefined ports_range config ~n",?NOTICE), ok; _ -> ?LOG("Start client port server on remote nodes ~n",?NOTICE), %% first, get a single erlang node per host, and start the cport gen_server on this node UNodes = get_one_node_per_host(RemoteNodes), SetParams = fun(Node) -> {ok, MyHostName} =ts_utils:node_to_hostname(Node), {Node, "cport-" ++ MyHostName} end, CPorts = lists:map(SetParams, UNodes), ?LOGF("Will run start_cport with arg:~p ~n",[CPorts],?DEB), lists:foreach(fun ts_sup:start_cport/1 ,CPorts) end, lists:foreach(StartLaunchers, RemoteNodes), set_max_duration(Config#config.duration), {noreply, State#state{last_beam_id = LastId}}; Bad -> ?LOGF("Can't start tsung application on all remote clients, abort ~p~n",[Bad],?ERR), ts_mon:abort(), {stop,normal,State} end end; %% use_controller_vm and max number of concurrent users reached , big trouble ! handle_cast({newbeam, Host, _}, State=#state{ hostname=LocalHost,config=Config}) when Config#config.use_controller_vm and ( ( LocalHost == Host ) or ( Host == 'localhost' )) -> Msg ="Maximum number of concurrent users in a single VM reached and 'use_controller_vm' is true, can't start new beam !!! Check 'maxusers' value in configuration.~n", ?LOG(Msg, ?EMERG), erlang:display(Msg), {noreply, State}; %% start a launcher on a new beam with slave module handle_cast({newbeam, Host, Arrivals}, State=#state{last_beam_id = NodeId, config=Config, logdir = LogDir}) -> Args = set_remote_args(LogDir,Config#config.ports_range), Seed = Config#config.seed, Node = remote_launcher(Host, NodeId, Args), case rpc:call(Node,tsung,start,[],?RPC_TIMEOUT) of {badrpc, Reason} -> ?LOGF("Fail to start tsung on beam ~p, reason: ~p",[Node,Reason], ?ERR), slave:stop(Node), {noreply, State}; _ -> ts_launcher_static:stop(Node), % no need for static launcher in this case (already have one) ts_launcher:launch({Node, Arrivals, Seed}), {noreply, State#state{last_beam_id = NodeId+1}} end; handle_cast({end_launching, _Node}, State=#state{ending_beams=Beams}) -> {noreply, State#state{ending_beams = Beams+1}}; handle_cast({abort}, State) -> ts_mon:abort(), ?LOG("Tsung test aborted by request !~n",?EMERG), {stop, normal, State}; handle_cast(Msg, State) -> ?LOGF("Unknown cast ~p ! ~n",[Msg],?WARN), {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info({timeout, _Ref, end_tsung}, State) -> ts_mon:abort(), ?LOG("Tsung test max duration reached, exits ! ~n",?EMERG), {stop, normal, State}; handle_info({'EXIT', _Pid, {slave_failure,timeout}}, State) -> ts_mon:abort(), ?LOG("Abort ! ~n",?EMERG), {stop, normal, State}; handle_info({'EXIT', Pid, normal}, State) -> ?LOGF("spawned process termination (~p) ~n",[Pid],?INFO), {noreply, State}; handle_info({'ETS-TRANSFER',Tab,_FromPid,GiftData}, State=#state{config=Config}) -> {noreply, State#state{config=Config#config{job_notify_port={Tab,GiftData}}}}; handle_info(Info, State) -> ?LOGF("Unknown info ~p ! ~n",[Info],?WARN), {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- %% @spec is_vm_local(Host::atom(),Localhost::atom(),UseController::boolean()) -> boolean() is_vm_local(Host,Host,true) -> true; is_vm_local('localhost',_,true) -> true; is_vm_local(_,_,_) -> false. set_start_date(undefined)-> ts_utils:add_time(?TIMESTAMP, ?config(warm_time)); set_start_date(Date) -> Date. get_user_param(Client,Config)-> {ok,IP} = choose_client_ip(Client), {ok, Server} = choose_server(Config#config.servers, Config#config.total_server_weights), CPort = choose_port(IP, Config#config.ports_range), { {IP, CPort}, Server}. %%---------------------------------------------------------------------- %% Func: choose_client_ip/1 %% Args: #client %% Purpose: choose an IP for a client %% Returns: {ok, IP} IP=IP address %%---------------------------------------------------------------------- choose_client_ip(#client{ip = IPList, host=Host, iprange = undefined}) -> choose_rr(IPList, Host, {0,0,0,0}); choose_client_ip(#client{iprange = {A,B,C,D}}) -> RangeToValue = fun(I) when is_integer(I) -> I; ({Min,Max}) -> ts_stats:uniform(Min,Max) end, IPs = lists:map(RangeToValue, [A,B,C,D]), {ok, list_to_tuple(IPs)}. %%---------------------------------------------------------------------- %% Func: choose_server/1 %% Args: List %% Purpose: choose a server for a new client %% Returns: {ok, #server} %%---------------------------------------------------------------------- choose_server([Server], _TotalWeight) -> {ok, Server}; choose_server(Servers, Total) -> choose_server(Servers, random:uniform() * Total, 0). choose_server([S=#server{weight=P} | _],Rand,Cur) when Rand =< P+Cur-> {ok, S}; choose_server([#server{weight=P} | SList], Rand, Cur) -> choose_server(SList, Rand, Cur+P). %%---------------------------------------------------------------------- %% Func: choose_rr/3 %% Args: List, Key, Default %% Purpose: choose an value in list in a round robin way. Use last %% value stored in the process dictionary % Return Default if list is empty %% Returns: {ok, Val} %%---------------------------------------------------------------------- choose_rr([],_, Def) -> % no val, return default {ok, Def}; choose_rr([Val],_,_) -> % only one value {ok, Val}; choose_rr(List, Key, _) -> I = case get({rr,Key}) of undefined -> 1 ; % first use of this key, init index to 1 Val when is_integer(Val) -> (Val rem length(List))+1 % round robin end, put({rr, Key},I), {ok, lists:nth(I, List)}. %%---------------------------------------------------------------------- %% Func: choose_session/2 %% Args: List of #session %% Purpose: choose an session randomly %% Returns: #session %%---------------------------------------------------------------------- choose_session([Session], _Total, _PhaseId) -> %% only one Session {ok, Session}; choose_session(Sessions,Total,PhaseId) when is_number(Total)-> choose_session(Sessions, random:uniform() * Total, 0, PhaseId); choose_session(Sessions,Total,PhaseId) when is_list(Total) -> choose_session(Sessions, random:uniform() * lists:nth(PhaseId, Total), 0, PhaseId). choose_session([S=#session{popularity=P} | _],Rand,Cur,_PhaseId) when is_number(P) andalso Rand =< P+Cur-> {ok, S}; choose_session([#session{popularity=P} | SList], Rand, Cur, PhaseId) when is_number(P)-> choose_session(SList, Rand, Cur+P, PhaseId); choose_session([S=#session{popularity=PopList} | SList],Rand,Cur,PhaseId) -> P = lists:nth(PhaseId,PopList), if Rand =< P+Cur -> {ok, S}; true -> choose_session(SList, Rand, Cur+P, PhaseId) end. %%---------------------------------------------------------------------- %% @spec get_client_cfg(ArrivalPhase::record(arrivalphase), %% Acc::{Total_weight::integer(),Client::record(client),IsLast::binary()}) -> %% {{UpdatedPhase::record(arrivalphase),{Intensity::number(),NUsers::integer(),Duration::integer()}}, Acc} %% @doc set parameters for given host client and phase. %% @end get_client_cfg(Arrival=#arrivalphase{duration = Duration, intensity= PhaseIntensity, curnumber= CurNumber, wait_all_sessions_end = WaitSessionsEnd, maxnumber= MaxNumber }, {TotalWeight,Client,IsLast} ) -> Weight = Client#client.weight, ClientIntensity = PhaseIntensity * Weight / TotalWeight, NUsers = round(case MaxNumber of infinity -> %% only use the duration to set the number of users Duration * ClientIntensity; _ -> TmpMax = case {IsLast,CurNumber == MaxNumber} of {true,_} -> MaxNumber-CurNumber; {false,true} -> 0; {false,false} -> lists:max([1,trunc(MaxNumber * Weight / TotalWeight)]) end, lists:min([TmpMax, Duration*ClientIntensity]) end), ?LOGF("New arrival phase ~p for client ~p (last ? ~p): will start ~p users~n", [Arrival#arrivalphase.phase,Client#client.host, IsLast,NUsers],?NOTICE), Phase = #phase{intensity=ClientIntensity, nusers=NUsers, duration= Duration, wait_all_sessions_end = WaitSessionsEnd }, {Arrival#arrivalphase{curnumber=CurNumber+NUsers}, Phase}. %%---------------------------------------------------------------------- %% Func: encode_filename/1 %% Purpose: kludge: the command line erl doesn't like special characters %% in strings when setting up environment variables for application, %% so we encode these characters ! %%---------------------------------------------------------------------- encode_filename(String) when is_list(String)-> Transform=[{"\\.","_46"},{"\/","_47"},{"\-","_45"}, {"\:","_58"}, {",","_44"}], lists:foldl(fun replace_str/2, "ts_encoded" ++ String, Transform); encode_filename(Term) -> Term. %%---------------------------------------------------------------------- %% Func: decode_filename/1 %%---------------------------------------------------------------------- decode_filename("ts_encoded" ++ String)-> Transform=[{"_46","."},{"_47","\/"},{"_45","\-"}, {"_58","\:"}, {"_44",","}], lists:foldl(fun replace_str/2, String, Transform). replace_str({A,B},X) -> re:replace(X,A,B,[{return,list},global]). %%---------------------------------------------------------------------- %% Func: print_info/0 Print system info %%---------------------------------------------------------------------- print_info() -> VSN = case lists:keysearch(tsung_controller,1,application:loaded_applications()) of {value, {_,_ ,V}} -> V; _ -> "unknown" end, ?LOGF("SYSINFO:Tsung version: ~s~n",[VSN],?WARN), ?LOGF("SYSINFO:Erlang version: ~s~n",[erlang:system_info(system_version)],?WARN), ?LOGF("SYSINFO:System architecture ~s~n",[erlang:system_info(system_architecture)],?WARN), ?LOGF("SYSINFO:Current path: ~s~n",[code:which(tsung)],?WARN). %%---------------------------------------------------------------------- %% Func: start_file_server/1 %%---------------------------------------------------------------------- start_file_server(#config{file_server=[]}) -> ?LOG("No File server defined, skip~n",?DEB); start_file_server(Config=#config{file_server=Filenames}) -> ?LOG("Starting File server~n",?INFO), FileSrv = {ts_file_server, {ts_file_server, start, []}, transient, 2000, worker, [ts_msg_server]}, supervisor:start_child(ts_controller_sup, FileSrv), ts_file_server:read(Filenames), ?LOG("Starting user servers if needed~n",?INFO), setup_user_servers(Config#config.vhost_file,Config#config.user_server_maxuid). %%---------------------------------------------------------------------- %% Func: setup_user_servers/2 %%---------------------------------------------------------------------- setup_user_servers(_,none) -> ?LOG("Don't start any user server, as user_server_maxuid not defined~n",?DEB), ok; setup_user_servers(none,Val) when is_integer(Val) -> ts_user_server:reset(Val); setup_user_servers(FileId,Val) when is_atom(FileId), is_integer(Val) -> ?LOGF("Starting user servers with params ~p ~p~n",[FileId,Val],?DEB), {ok,Domains} = ts_file_server:get_all_lines(FileId), ?LOGF("Domains:~p~n",[Domains],?DEB), lists:foreach(fun(Domain) -> {ok,_} = ts_user_server_sup:start_user_server(list_to_atom("us_" ++binary_to_list(Domain))) end, Domains), ts_user_server:reset_all(Val). %%---------------------------------------------------------------------- %% Func: check_config/1 %% Returns: ok | {error, ErrorList} %%---------------------------------------------------------------------- check_config(Config=#config{use_weights=UseWeights})-> case lists:dropwhile(fun(Pop) -> check_popularity(UseWeights,Pop) == ok end, Config#config.total_popularity) of [] -> ts_config_http:check_user_agent_sum(Config#config.session_tab); [BadPop|_] -> {error, {bad_sum, BadPop, ?SESSION_POP_ERROR_MSG}} end. check_popularity(false, Val) when abs(100-Val) < 0.05 -> ok; check_popularity(false,_Val) -> {error, bad_sum }; check_popularity(true, _Val) -> ok. load_app(Name) when is_atom(Name) -> FName = atom_to_list(Name) ++ ".app", case code:where_is_file(FName) of non_existing -> {error, {file:format_error(error_enoent), FName}}; FullName -> case file:consult(FullName) of {ok, [Application]} -> {ok, Application}; {error, Reason} -> {error, {file:format_error(Reason), FName}} end end. %%---------------------------------------------------------------------- %% Func: loop_load/1 %% Args: #config %% Returns: #config %% Purpose: duplicate phases 'load_loop' times. %%---------------------------------------------------------------------- loop_load(Config=#config{load_loop=Loop,arrivalphases=Arrival}) when is_integer(Loop) -> Sorted=lists:keysort(#arrivalphase.phase, Arrival), {SortedWithId,_} = lists:mapfoldl(fun(Phase, Id) -> {Phase#arrivalphase{id=Id}, Id+1} end, 1, Sorted), loop_load(Config#config{arrivalphases=SortedWithId}, ts_utils:keymax(#arrivalphase.phase, Arrival), SortedWithId ). %% We have a list of n phases: duplicate the list and increase by the %% max to get a new unique id for all phases. Here we don't care about %% the order, so we start with the last iteration (Loop* Max) loop_load(Config=#config{load_loop=0},_,Current) -> Sorted=lists:keysort(#arrivalphase.phase, Current), ?LOGF("sorted phases: ~p ~n", [Sorted], ?DEB), Config#config{arrivalphases=Sorted}; loop_load(Config=#config{load_loop=Loop, arrivalphases=Arrival},Max,Current) -> Fun= fun(Phase) -> Phase+Max*Loop end, NewArrival = lists:keymap(Fun,#arrivalphase.phase,Arrival), loop_load(Config#config{load_loop=Loop-1},Max,lists:append(Current, NewArrival)). %% @doc sort static users by start time sort_static(Config=#config{static_users=S})-> ?LOGF("sort static users: ~p ~n", [S], ?DEB), ES = expand_static(S,Config#config.sessions), SortedL= lists:keysort(1,ES), Config#config{static_users=static_name_to_session(Config#config.sessions,SortedL)}. %% expand static users (if it contains wildcards) expand_static(StaticUsers, Sessions) -> Names = lists:map(fun(#session{name=A}) -> A end ,Sessions), expand_static(StaticUsers, Names, []). expand_static([], _Names, Static) -> Static; expand_static([{Delay, Name} | Static],SessionsNames, Acc) -> Names = ts_utils:wildcard(Name, SessionsNames), NewStatic = lists:map(fun(N) -> {Delay, N} end, Names), expand_static(Static, SessionsNames, Acc ++ NewStatic). %% %% @doc start a remote beam %% start_slave(Host, Name, Args) when is_atom(Host), is_atom(Name)-> case slave:start(Host, Name, Args) of {ok, Node} -> ?LOGF("Remote beam started on node ~p ~n", [Node], ?NOTICE), Res = net_adm:ping(Node), ?LOGF("ping ~p ~p~n", [Node,Res], ?INFO), Node; {error, Reason} -> ?LOGF("Can't start newbeam on host ~p (reason: ~p) ! Aborting!~n",[Host, Reason],?EMERG), {error, {slave_failure, Reason}} end. choose_port(_,_, undefined) -> {[],0}; choose_port(Client,undefined, Range) -> choose_port(Client,dict:new(), Range); choose_port(ClientIp,Ports, {Min, Max}) -> case dict:find(ClientIp,Ports) of {ok, Val} when Val =< Max -> NewPorts=dict:update_counter(ClientIp,1,Ports), {NewPorts,Val}; _ -> % Max Reached or new entry NewPorts=dict:store(ClientIp,Min+1,Ports), {NewPorts,Min} end. choose_port(_,undefined) -> 0; choose_port(_, _Range) -> -1. %% @spec static_name_to_session(Sessions::list(), Static::list() ) -> StaticUsers::list() %% @doc convert session name to session id in static users list @end static_name_to_session(Sessions, Static) -> ?LOGF("Static users with session id ~p~n",[Static],?DEB), Search = fun({Delay,Name})-> {value, Session} = lists:keysearch(Name, #session.name, Sessions), {Delay, Session} end, Res=lists:map(Search, Static), ?LOGF("Static users with session id ~p~n",[Res],?DEB), Res. %% @spec set_nodename(NodeId::integer()) -> string() %% @doc set slave node name: check if controller node name has an id, %% and put it in the slave name set_nodename(NodeId) when is_integer(NodeId)-> CId = case atom_to_list(node()) of "tsung_controller@"++_ -> ""; "tsung_controller"++Tail -> [Id|_] = string:tokens(Tail,"@"), Id++"_" end, list_to_atom("tsung"++ CId++ integer_to_list(NodeId)). %% @spec set_max_duration(integer()) -> ok %% @doc start a timer for the maximum duration of the load test. The %% maximum duration is 49 days set_max_duration(0) -> ok; % nothing to do set_max_duration(Duration) when Duration =< 4294967 -> ?LOGF("Set max duration of test: ~p s ~n",[Duration],?NOTICE), erlang:start_timer((Duration+?config(warm_time))*1000, self(), end_tsung ). local_launcher([],_,_) -> 0; local_launcher([Host],LogDir,Config) -> ?LOGF("Start a launcher on the controller beam ~p~n", [Host], ?NOTICE), LogDirEnc = encode_filename(LogDir), %% set the application spec (read the app file and update some env. var.) {ok, {_,_,AppSpec}} = load_app(tsung), {value, {env, OldEnv}} = lists:keysearch(env, 1, AppSpec), NewEnv = [ {debug_level,?config(debug_level)}, {log_file,LogDirEnc}], RepKeyFun = fun(Tuple, List) -> lists:keyreplace(element(1, Tuple), 1, List, Tuple) end, Env = lists:foldl(RepKeyFun, OldEnv, NewEnv), NewAppSpec = lists:keyreplace(env, 1, AppSpec, {env, Env}), ok = application:load({application, tsung, NewAppSpec}), case application:start(tsung) of ok -> ?LOG("Application started, activate launcher, ~n", ?INFO), application:set_env(tsung, debug_level, Config#config.loglevel), case Config#config.ports_range of {Min, Max} -> application:set_env(tsung, cport_min, Min), application:set_env(tsung, cport_max, Max); undefined -> "" end, ts_launcher_static:launch({node(), Host, []}), ts_launcher:launch({node(), Host, [], Config#config.seed}), 1 ; {error, Reason} -> ?LOGF("Can't start launcher application (reason: ~p) ! Aborting!~n",[Reason],?EMERG), {error, Reason} end. remote_launcher(Host, NodeId, Args) when is_list(Host)-> remote_launcher(list_to_atom(Host), NodeId, Args); remote_launcher(Host, NodeId, Args) when is_list(NodeId)-> remote_launcher(Host, list_to_integer(NodeId), Args); remote_launcher(Host, NodeId, Args)-> Name = set_nodename(NodeId), ?LOGF("starting newbeam ~p on host ~p with Args ~p~n", [Name, Host, Args], ?INFO), start_slave(Host, Name, Args). check_remotes_ok(Remotes) -> lists:foreach(fun({error, Reason}) -> ts_mon:abort(), exit(Reason); (_) -> ok end, Remotes). set_remote_args(LogDir,PortsRange)-> {ok, PAList} = init:get_argument(pa), PA = lists:flatmap(fun(A) -> [" -pa "] ++A end,PAList), ?DebugF("PA list ~p ~n", [PA]), Sys_Args= ts_utils:erl_system_args(), LogDirEnc = encode_filename(LogDir), Ports = case PortsRange of {Min, Max} -> " -tsung cport_min " ++ integer_to_list(Min) ++ " -tsung cport_max " ++ integer_to_list(Max); undefined -> "" end, lists:flatten([ Sys_Args, PA, " +K true ", " -tsung debug_level ", integer_to_list(?config(debug_level)), " -tsung log_file ", LogDirEnc, Ports ]). %% @spec get_one_node_per_host(RemoteNodes::list()) -> Nodes::list() %% @doc From a list if erlang nodenames, return a list with only a %% single node per host %% @end get_one_node_per_host([]) -> %%no remote nodes, we are using a controller vm [node()]; get_one_node_per_host(RemoteNodes) -> get_one_node_per_host(RemoteNodes,dict:new()) . get_one_node_per_host([], Dict) -> {_,Nodes} = lists:unzip(dict:to_list(Dict)), Nodes; get_one_node_per_host([Node | Nodes], Dict) -> Host = ts_utils:node_to_hostname(Node), case dict:is_key(Host, Dict) of true -> get_one_node_per_host(Nodes,Dict); false -> NewDict = dict:store(Host, Node, Dict), get_one_node_per_host(Nodes,NewDict) end. %% compute popularities of sessions for all phases compute_popularities(Config=#config{arrivalphases=Phases, sessions=Sessions}) -> %% popularities can contains wildcards, need to expand them Names = lists:map(fun(#session{name=A}) -> A end ,Sessions), Expand = fun( Phase = #arrivalphase{popularities= Pops} ) -> NewPops = lists:foldl(fun({Name,Popularity},Acc) -> Expanded = ts_utils:wildcard(Name, Names), Acc ++ lists:map(fun(X) -> {X, Popularity} end, Expanded) end, [], Pops), Phase#arrivalphase{popularities=NewPops} end, NewPhases = lists:map(Expand,Phases), ?LOGF("Compute popularities per phases ~p",[NewPhases],?DEB), F = fun(Session=#session{popularity=Pop, name=Name}) -> NewPop = set_pop(Name, Pop, NewPhases), Session#session{popularity=NewPop} end, NewSessions = lists:map(F, Sessions), ?LOGF("Old sessions:~p",[Sessions],?DEB), ?LOGF("New sessions:~p",[NewSessions],?DEB), Config#config{sessions=NewSessions, arrivalphases = NewPhases, total_popularity=update_total_pop(Config#config.use_weights, NewPhases, NewSessions)}. update_total_pop(UseWeight,Phases, Sessions) -> update_total_pop(UseWeight, length(Phases), Sessions, []). update_total_pop(_UseWeight,0, _, Total) -> ?LOGF("New Total popularities:~w",[Total],?DEB), Total; update_total_pop(UseWeight,N, Sessions, Total) -> Sum = fun(#session{popularity=P},Acc) when is_number(P) -> Acc+P ; (#session{popularity=L},Acc) -> Acc+lists:nth(N,L) end, PhaseTotal = lists:foldl(Sum, 0, Sessions), update_total_pop(UseWeight, N-1, Sessions, [PhaseTotal |Total]). %% set popularity of session 'Name' per phase (needed when is used) set_pop(_Name,Popularity,[]) -> Popularity; set_pop(Name,Popularity,Phases) -> set_pop(Name,Popularity,Phases,[]). set_pop(_Name,_Popularity,[], Acc) -> %% optimization: if all values are equal, return a single value and not a list Min=lists:min(Acc), case lists:max(Acc) of Min -> Min; % min = max _ -> lists:reverse(Acc) end; set_pop(Name,Popularity,[#arrivalphase{popularities=Pop}|Tail], Acc) -> New = case lists:keysearch(Name,1,Pop) of false -> Popularity; {value, {_, Val}} -> Val end, set_pop(Name,Popularity,Tail, [New|Acc]). %% Given a list of hostname with duplicates (e.g. when cpu is > 1 or %% batch), try to spread the duplicates in the list, in order to start %% remote beams (with pmap) on different hosts, otherwise we will %% start several beam on the same hosts, increasing the load, and %% slowing down the remote nodes starting phase. spread_nodelist(L) when length(L) < 10 -> %% small list, don't bother spreading anything L; spread_nodelist(List) -> ts_utils:spread_list(List). tsung-1.8.0/src/tsung_controller/ts_config_raw.erl0000644000201100017670000000647614377756736022135 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2004 %%% %%% Author : Nicolas Niclausse %%% Created: 20 Apr 2004 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% common functions used by http clients to parse config -module(ts_config_raw). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_raw.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=raw, attributes=Attrs}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Ack = ts_config:getAttr(atom,Attrs, ack, no_ack), Req = case ts_config:getAttr(string,Attrs, datasize) of [] -> Data = ts_config:getAttr(string,Attrs, data), #raw{data=Data}; "%%" ++ Tail -> #raw{datasize="%%"++Tail}; _ -> % datasize is not a dynamic variable; must be an integer #raw{datasize=ts_config:getAttr(integer,Attrs, datasize)} end, ts_config:mark_prev_req(Id-1, Tab, CurS), Msg=#ts_request{ack = Ack, subst = SubstFlag, match = MatchRegExp, param = Req}, ets:insert(Tab,{{CurS#session.id, Id},Msg#ts_request{endpage=true, dynvar_specs=DynVar}}), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_pgsql.erl0000644000201100017670000001700014377756736022453 0ustar nniclausdream%%% %%% Copyright © Nicolas Niclausse 2005 %%% %%% Author : Nicolas Niclausse %%% Created: 6 Nov 2005 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% common functions used by pgsql clients to parse config -module(ts_config_pgsql). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_pgsql.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=pgsql}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> {Ack,Request} = case ts_config:getAttr(atom, Element#xmlElement.attributes, type) of sql -> ValRaw = ts_config:getText(Element#xmlElement.content), SQL = list_to_binary(ts_utils:clean_str(ValRaw)), ?LOGF("Got SQL query: ~p~n",[SQL], ?NOTICE), {parse,#pgsql_request{sql=SQL, type= sql}}; close -> {parse,#pgsql_request{type=close}}; sync -> {parse,#pgsql_request{type=sync}}; flush -> {parse,#pgsql_request{type=flush}}; copydone -> {parse,#pgsql_request{type=copydone}}; execute -> Portal = ts_config:getAttr(Element#xmlElement.attributes, name_portal), Limit = ts_config:getAttr(integer,Element#xmlElement.attributes, max_rows,0), {no_ack,#pgsql_request{type=execute,name_portal=Portal,max_rows=Limit}}; parse -> Name = ts_config:getAttr(Element#xmlElement.attributes, name_prepared), Query = list_to_binary(ts_config:getText(Element#xmlElement.content)), Params=case ts_config:getAttr(Element#xmlElement.attributes, parameters) of "" -> ""; P -> lists:map(fun(S)-> list_to_integer(S) end, ts_utils:splitchar(P,$,)) end, {no_ack,#pgsql_request{type=parse,name_prepared=Name,equery=Query,parameters=Params}}; bind -> Portal = ts_config:getAttr(Element#xmlElement.attributes, name_portal), Prep = ts_config:getAttr(Element#xmlElement.attributes, name_prepared), Formats = case ts_config:getAttr(Element#xmlElement.attributes, formats_results) of "" -> ""; FR -> lists:map(fun(A)->list_to_atom(A) end,ts_utils:splitchar(FR,$,)) end, Params=case ts_config:getAttr(Element#xmlElement.attributes, parameters) of "" -> ""; P -> lists:map(fun("null")-> null; (A) -> A end, ts_utils:split(P,",")) end, ParamsFormat = ts_config:getAttr(atom,Element#xmlElement.attributes, formats, none), {no_ack,#pgsql_request{type=bind,name_portal=Portal,name_prepared=Prep, formats=ParamsFormat,formats_results=Formats,parameters=Params}}; copy -> Contents = case ts_config:getAttr(string, Element#xmlElement.attributes, contents_from_file) of [] -> P=ts_config:getText(Element#xmlElement.content), list_to_binary(lists:map(fun(S)-> list_to_integer(S) end, ts_utils:splitchar(P,$,))); FileName -> {ok, FileContent} = file:read_file(FileName), FileContent end, {no_ack,#pgsql_request{type=copy,equery=Contents}}; copyfail -> Str = ts_config:getAttr(string,Element#xmlElement.attributes, equery,undefined), {parse,#pgsql_request{type=copyfail,equery=list_to_binary(Str)}}; describe -> Portal=ts_config:getAttr(string,Element#xmlElement.attributes, name_portal,undefined), Prep = ts_config:getAttr(string,Element#xmlElement.attributes, name_prepared,undefined), {no_ack,#pgsql_request{type=describe,name_portal=Portal,name_prepared=Prep}}; authenticate -> Passwd = ts_config:getAttr(Element#xmlElement.attributes, password), {parse,#pgsql_request{passwd=Passwd, type= authenticate}}; connect -> Database = ts_config:getAttr(Element#xmlElement.attributes, database), User = ts_config:getAttr(Element#xmlElement.attributes, username), {parse,#pgsql_request{username=User, database=Database, type=connect}} end, Msg= #ts_request{ack = Ack, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id}, Msg }), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing options %% parse_config(Element = #xmlElement{name=options}, Conf = #config{session_tab = Tab}) -> %% case ts_config:getAttr(Element#xmlElement.attributes, name) of %% "todo" -> %% Val = ts_config:getAttr(Element#xmlElement.attributes, value), %% ets:insert(Tab,{{http_use_server_as_proxy}, Val}) %% end, %% lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Conf, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_mysql.erl0000644000201100017670000000743414377756736022504 0ustar nniclausdream%%% Created: July 2008 by Grégoire Reboul %%% From : ts_config_pgsql.erl by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. -module(ts_config_mysql). -author('gregoire.reboul@laposte.net'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_mysql.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=mysql}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Request = case ts_config:getAttr(atom, Element#xmlElement.attributes, type) of sql -> ValRaw = ts_config:getText(Element#xmlElement.content), SQL = ts_utils:clean_str(ValRaw), ?LOGF("Got SQL query: ~p~n",[SQL], ?NOTICE), #mysql_request{sql=SQL, type= sql}; close -> ?LOGF("Got Close queryp~n",[], ?NOTICE), #mysql_request{type= close}; authenticate -> Database = ts_config:getAttr(Element#xmlElement.attributes, database), User = ts_config:getAttr(Element#xmlElement.attributes, username), Passwd = ts_config:getAttr(Element#xmlElement.attributes, password), ?LOGF("Got Auth data: database->~p user->~p password->~p~n",[Database,User,Passwd], ?NOTICE), #mysql_request{username=User, database=Database, passwd=Passwd, type=authenticate}; connect -> ?LOGF("Got Connect ~n",[], ?NOTICE), #mysql_request{type=connect} end, Msg= #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id}, Msg }), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_mqtt.erl0000644000201100017670000001262514377756736022322 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_mqtt). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_mqtt.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name = dyn_variable}, Conf = #config{}) -> ts_config:parse(Element, Conf); parse_config(Element = #xmlElement{name = mqtt}, Config = #config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar = DynVar, subst = SubstFlag, match = MatchRegExp}) -> Type = ts_config:getAttr(atom, Element#xmlElement.attributes, type), CleanStart = ts_config:getAttr(atom, Element#xmlElement.attributes, clean_start, true), UserName = ts_config:getAttr(string, Element#xmlElement.attributes, username, undefined), Password = ts_config:getAttr(string, Element#xmlElement.attributes, password, undefined), ClientId = ts_config:getAttr(string, Element#xmlElement.attributes, client_id, undefined), KeepAlive = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, keepalive, 10), WillTopic = ts_config:getAttr(string, Element#xmlElement.attributes, will_topic, ""), WillQos = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, will_qos, 0), WillMsg = ts_config:getAttr(string, Element#xmlElement.attributes, will_msg, ""), WillRetain = ts_config:getAttr(atom, Element#xmlElement.attributes, will_retain, false), Topic = ts_config:getAttr(string, Element#xmlElement.attributes, topic, ""), Qos = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, qos, 0), Retained = ts_config:getAttr(atom, Element#xmlElement.attributes, retained, false), Stamped = ts_config:getAttr(atom, Element#xmlElement.attributes, stamped, false), RetainValue = case Retained of true -> 1; false -> 0 end, Timeout = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, timeout, 1), Payload = ts_config:getText(Element#xmlElement.content), Request = #mqtt_request{type = Type, clean_start = CleanStart, keepalive = KeepAlive, will_topic = WillTopic, will_qos = WillQos, will_msg = WillMsg, will_retain = WillRetain, topic = Topic, qos = Qos, retained = RetainValue, payload = Payload, username = UserName, password = Password, stamped = Stamped, client_id = ClientId}, Ack = case {Type, Qos} of {publish, 0} -> no_ack; {disconnect, _} -> no_ack; _ -> parse end, Msg = #ts_request{ack = Ack, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), case Type of waitForMessages -> ets:insert(Tab, {{CurS#session.id, Id}, {thinktime, Timeout * 1000}}); _ -> ets:insert(Tab, {{CurS#session.id, Id}, Msg }) end, ?LOGF("request tab: ~p~n", [ets:match(Tab, '$1')], ?INFO), lists:foldl( fun(A, B)->ts_config:parse(A, B) end, Config#config{dynvar = []}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_ldap.erl0000644000201100017670000002061514377756736022253 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% File : ts_ldap.erl %%% Author : Pablo Polvorin %%% Purpose : LDAP plugin -module(ts_config_ldap). -export([ parse_config/2 ]). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). -include("ts_ldap.hrl"). %%---------------------------------------------------------------- %%-----Configuration parsing %%---------------------------------------------------------------- parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=ldap}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Request = case ts_config:getAttr(atom, Element#xmlElement.attributes, type) of start_tls -> Cacert = ts_config:getAttr(string,Element#xmlElement.attributes,cacertfile), KeyFile = ts_config:getAttr(string,Element#xmlElement.attributes,keyfile), CertFile = ts_config:getAttr(string,Element#xmlElement.attributes,certfile), #ts_request{ack = parse, endpage = true, dynvar_specs= DynVar, subst = SubstFlag, match= MatchRegExp, param = #ldap_request{type=start_tls,cacertfile=Cacert,keyfile=KeyFile,certfile=CertFile}}; bind -> User = ts_config:getAttr(string,Element#xmlElement.attributes,user), Password = ts_config:getAttr(string,Element#xmlElement.attributes,password), #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = #ldap_request{type=bind,user=User,password=Password}}; unbind -> #ts_request{ack = no_ack, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = #ldap_request{type=unbind}}; search -> Base = ts_config:getAttr(string,Element#xmlElement.attributes,base), Scope = ts_config:getAttr(atom,Element#xmlElement.attributes,scope), Filter = ts_config:getAttr(string,Element#xmlElement.attributes,filter), ResultVar = case ts_config:getAttr(string,Element#xmlElement.attributes,result_var,none) of none -> none; VarName -> {ok,list_to_atom(VarName)} end, Attributes=[], {ParsedFilter,[]} = rfc4515_parser:filter(rfc4515_parser:tokenize(Filter)), #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = #ldap_request{type=search, result_var = ResultVar, base=Base, scope=Scope, filter=ParsedFilter, attributes=Attributes}}; add -> DN = ts_config:getAttr(string,Element#xmlElement.attributes,dn), XMLAttrs = [El || El <- Element#xmlElement.content, is_record(El,xmlElement)], Attrs = lists:map(fun(#xmlElement{name=attr,attributes=Attr,content=Content}) -> Vals = lists:foldl(fun(#xmlElement{name=value,content=[#xmlText{value=Value}]},Values) -> [binary_to_list(iolist_to_binary(Value))|Values] end,[] ,[E || E=#xmlElement{name=value} <- Content]), {ts_config:getAttr(string,Attr,type),Vals} end, XMLAttrs), #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = #ldap_request{type=add, dn=DN, attrs=Attrs }}; modify -> DN = ts_config:getAttr(string,Element#xmlElement.attributes,dn), Modifications = [{list_to_atom(ts_config:getAttr(string,Attrs,type)),parse_xml_attr_type_and_value(Content)} || #xmlElement{name=modification,attributes=Attrs,content=Content} <- Element#xmlElement.content], ExpandedModifications = lists:foldl( fun({Operation,Attrs},L) -> lists:foldl(fun({Type,Values},L2) -> [{Operation,Type,Values}|L2] end,L,Attrs) end,[],Modifications), #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = #ldap_request{type=modify, modifications = ExpandedModifications, dn=DN }} end, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id}, Request }), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. parse_xml_attr_values(Elements) -> [binary_to_list(iolist_to_binary(Value)) || #xmlElement{name=value,content=[#xmlText{value=Value}]} <- Elements]. parse_xml_attr_type_and_value(Elements) -> [ {ts_config:getAttr(string,Attr,type),parse_xml_attr_values(Content)} || #xmlElement{name=attr,attributes=Attr,content=Content} <-Elements]. tsung-1.8.0/src/tsung_controller/ts_config_job.erl0000644000201100017670000000776214377756736022115 0ustar nniclausdream%%% %%% Copyright 2011 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 4 mai 2011 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_job). -vc('$Id$ '). -author('nicolas.niclausse@inria.fr'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). -include("ts_job.hrl"). %% @spec parse_config(#xmlElement{}, Config::term()) -> NewConfig::term() %% @doc Parses a tsung.xml configuration file xml element for this %% protocol and updates the Config term. %% @end parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=job}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Request = #job{req = ts_config:getAttr(atom,Element#xmlElement.attributes, req, submit), type = ts_config:getAttr(atom,Element#xmlElement.attributes, type, oar), script = ts_config:getAttr(string,Element#xmlElement.attributes, script), notify_script = ts_config:getAttr(string,Element#xmlElement.attributes, notify_script), walltime = ts_config:getAttr(string,Element#xmlElement.attributes, walltime, "1:00:00"), resources = ts_config:getAttr(string,Element#xmlElement.attributes, resources, ""), queue = ts_config:getAttr(string,Element#xmlElement.attributes, queue), notify_port = ts_config:getAttr(integer_or_string,Element#xmlElement.attributes, notify_port), jobid = ts_config:getAttr(integer_or_string,Element#xmlElement.attributes, jobid, undefined), name = ts_config:getAttr(string,Element#xmlElement.attributes, name, "tsung"), user = ts_config:getAttr(string,Element#xmlElement.attributes, user, undefined), options = ts_config:getAttr(string,Element#xmlElement.attributes, options), duration = ts_config:getAttr(integer_or_string,Element#xmlElement.attributes, duration, 3600) }, Msg= #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id},Msg}), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config_jabber.erl0000644000201100017670000002737614377756736022573 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2004 %%% %%% Author : Nicolas Niclausse %%% Created: 20 Apr 2004 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_jabber). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([parse_config/2 ]). -include("ts_profile.hrl"). -include("ts_jabber.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% TODO: Dynamic content substitution is not yet supported for Jabber parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=jabber}, Config=#config{curid= Id, session_tab = Tab, match=MatchRegExp, dynvar=DynVar, subst= SubstFlag, sessions = [CurS |_]}) -> initialize_options(Tab), TypeStr = ts_config:getAttr(string,Element#xmlElement.attributes, type, "chat"), Ack = ts_config:getAttr(atom,Element#xmlElement.attributes, ack, no_ack), Dest= ts_config:getAttr(atom,Element#xmlElement.attributes, destination,random), Stamped = ts_config:getAttr(atom,Element#xmlElement.attributes, stamped, false), Size= ts_config:getAttr(integer,Element#xmlElement.attributes, size,0), Data= ts_config:getAttr(string,Element#xmlElement.attributes, data,undefined), Show= ts_config:getAttr(string,Element#xmlElement.attributes, show, "chat"), Status= ts_config:getAttr(string,Element#xmlElement.attributes, status, "Available"), Resource= ts_config:getAttr(string,Element#xmlElement.attributes, resource, "tsung"), Type= list_to_atom(TypeStr), Version = ts_config:getAttr(string,Element#xmlElement.attributes, version, "1.0"), Cacert = ts_config:getAttr(string,Element#xmlElement.attributes, cacertfile, undefined), KeyFile = ts_config:getAttr(string,Element#xmlElement.attributes, keyfile, undefined), KeyPass = ts_config:getAttr(string,Element#xmlElement.attributes, keypass, undefined), CertFile = ts_config:getAttr(string,Element#xmlElement.attributes, certfile, undefined), Room = ts_config:getAttr(string,Element#xmlElement.attributes, room, undefined), Nick = ts_config:getAttr(string,Element#xmlElement.attributes, nick, undefined), Group = ts_config:getAttr(string,Element#xmlElement.attributes, group, "Tsung Group"), RE = ts_config:getAttr(string,Element#xmlElement.attributes, regexp, undefined), Node = case ts_config:getAttr(string, Element#xmlElement.attributes, 'node', undefined) of "" -> user_root; X -> X end, NodeType = ts_config:getAttr(string, Element#xmlElement.attributes, 'node_type', undefined), %% This specify where the node identified in the 'node' attribute is located. %% If node is undefined (no node attribute) %% -> we don't specify the node, let the server choose one for us. %% else %% If node is absolute (starts with "/") %% use that absolute address %% else %% the address is relative. Composed of two variables: user and node %% if node is "" (attribute node="") %% we want the "root" node for that user (/home/domain/user) %% else %% we want a specific child node for that user (/home/domain/user/node) %% in both cases, the user is obtained as: %% if dest == "random" %% random_user() %% if dest == "online" %% online_user() %% if dest == "offline" %% offline_user() %% Otherwise: (any other string) %% The specified string SubId = ts_config:getAttr(string, Element#xmlElement.attributes, 'subid', undefined), Domain =ts_config:get_default(Tab, jabber_domain_name, jabber_domain), ?LOGF("XMPP domain is ~p~n",[Domain],?DEB), MUC_service = ts_config:get_default(Tab, muc_service), PubSub_service =ts_config:get_default(Tab, pubsub_service), UserPrefix=ts_config:get_default(Tab, jabber_username), UserIdMax = ts_config:get_default(Tab, jabber_userid_max), %% Authentication {XMPPId, UserName, Passwd} = case lists:keysearch(xmpp_authenticate, #xmlElement.name, Element#xmlElement.content) of {value, AuthEl=#xmlElement{} } -> User= ts_config:getAttr(string,AuthEl#xmlElement.attributes, username, undefined), PWD= ts_config:getAttr(string,AuthEl#xmlElement.attributes, passwd, undefined), {user_defined,User,PWD}; _ -> GPasswd =ts_config:get_default(Tab, jabber_passwd), {0,UserPrefix,GPasswd} end, Msg=#ts_request{ack = Ack, dynvar_specs= DynVar, endpage = true, subst = SubstFlag, match = MatchRegExp, param = #jabber{domain = Domain, username = UserName, passwd = Passwd, id = XMPPId, data = Data, type = Type, stamped = Stamped, regexp = RE, dest = Dest, size = Size, show = Show, status = Status, resource = Resource, room = Room, nick = Nick, group = Group, muc_service = MUC_service, pubsub_service = PubSub_service, node = Node, node_type = NodeType, subid = SubId, version = Version, cacertfile = Cacert, keyfile = KeyFile, keypass = KeyPass, certfile = CertFile, prefix = UserPrefix } }, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id}, Msg}), ?LOGF("Insert new request ~p, id is ~p~n",[Msg,Id],?INFO), lists:foldl( fun(A,B) -> ts_config:parse(A,B) end, Config#config{dynvar=[], user_server_maxuid = UserIdMax}, Element#xmlElement.content); %% Parsing options parse_config(Element = #xmlElement{name=option}, Conf = #config{session_tab = Tab}) -> NewConf = case ts_config:getAttr(Element#xmlElement.attributes, name) of "username" -> Val = ts_config:getAttr(string,Element#xmlElement.attributes, value,?xmpp_username), ets:insert(Tab,{{jabber_username,value}, Val}), Conf; "passwd" -> Val = ts_config:getAttr(string,Element#xmlElement.attributes, value,?xmpp_passwd), ets:insert(Tab,{{jabber_passwd,value}, Val}), Conf; "domain" -> Val = ts_config:getAttr(string,Element#xmlElement.attributes, value, ?xmpp_domain), ets:insert(Tab,{{jabber_domain_name,value}, {domain,Val}}), Conf; "vhost_file" -> Val = ts_config:getAttr(atom,Element#xmlElement.attributes, value,"vhostfile"), ets:insert_new(Tab,{{jabber_domain_name,value}, {vhost,Val}}), Conf#config{vhost_file = Val}; "global_number" -> N = ts_config:getAttr(integer,Element#xmlElement.attributes, value, ?xmpp_global_number), ets:insert(Tab,{{jabber_global_number, value}, N}), Conf; "userid_max" -> N = ts_config:getAttr(integer,Element#xmlElement.attributes, value, ?xmpp_userid_max), ts_user_server:reset(N), ets:insert(Tab,{{jabber_userid_max,value}, N}), Conf#config{user_server_maxuid = N}; "muc_service" -> N = ts_config:getAttr(string,Element#xmlElement.attributes, value, "conference.localhost"), ets:insert(Tab,{{muc_service,value}, N}), Conf; "pubsub_service" -> N = ts_config:getAttr(string,Element#xmlElement.attributes, value, "pubsub.localhost"), ets:insert(Tab,{{pubsub_service,value}, N}), Conf; "random_from_fileid" -> FileId = ts_config:getAttr(atom,Element#xmlElement.attributes, value, none), ?LOGF("set random fileid to ~p~n",[FileId],?WARN), ts_user_server:set_random_fileid(FileId), Conf; "offline_from_fileid" -> FileId = ts_config:getAttr(atom,Element#xmlElement.attributes, value, none), ?LOGF("set offline fileid to ~p~n",[FileId],?WARN), ts_user_server:set_offline_fileid(FileId), Conf; "fileid_delimiter" -> D = ts_config:getAttr(string,Element#xmlElement.attributes, value, ";"), ts_user_server:set_fileid_delimiter(list_to_binary(D)), Conf end, lists:foldl( fun(A,B) -> ts_config:parse(A,B) end, NewConf, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. initialize_options(Tab) -> case ts_config:get_default(Tab, jabber_initialized) of {undef_var,_} -> ets:insert_new(Tab,{{jabber_userid_max,value}, ?xmpp_userid_max}), ets:insert_new(Tab,{{jabber_global_number,value}, ?xmpp_global_number}), ets:insert_new(Tab,{{jabber_username,value}, ?xmpp_username}), ets:insert_new(Tab,{{jabber_passwd,value}, ?xmpp_passwd}), ets:insert_new(Tab,{{jabber_domain_name,value}, {domain,?xmpp_domain}}), ets:insert_new(Tab,{{jabber_initialized,value}, true}), ts_timer:config(ts_config:get_default(Tab, jabber_global_number)); _Else -> ok end. tsung-1.8.0/src/tsung_controller/ts_config_http.erl0000644000201100017670000005205214377756736022312 0ustar nniclausdream%%% %%% Copyright © IDEALX S.A.S. 2004 %%% %%% Author : Nicolas Niclausse %%% Created: 20 Apr 2004 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% common functions used by http clients to parse config -module(ts_config_http). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([parse_config/2, parse_URL/1, set_port/1, set_scheme/1, check_user_agent_sum/1, set_query/1, encode_ipv6_address/1, parse_headers/2]). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=http}, Config=#config{curid = Id, session_tab = Tab, servers = [Server|_] = Servers, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Version = ts_config:getAttr(string,Element#xmlElement.attributes, version, "1.1"), URL = ts_config:getAttr(Element#xmlElement.attributes, url), Contents = case ts_config:getAttr(string, Element#xmlElement.attributes, contents_from_file) of [] -> C=ts_config:getAttr(Element#xmlElement.attributes, contents), list_to_binary(C); FileName -> {ok, FileContent} = file:read_file(FileName), FileContent end, UseProxy = case ets:lookup(Tab,{http_use_server_as_proxy}) of [] -> false; _ -> true end, %% Apache Tomcat applications need content-type information to read post forms ContentType = ts_config:getAttr(string,Element#xmlElement.attributes, content_type, "application/x-www-form-urlencoded"), Date = ts_config:getAttr(string, Element#xmlElement.attributes, 'if_modified_since', undefined), Method = list_to_atom(ts_utils:to_lower(ts_config:getAttr(string, Element#xmlElement.attributes, method, "get"))), Request = #http_request{url = URL, method = Method, version = Version, get_ims_date= Date, content_type= ContentType, body = Contents, tag = Config#config.tag}, %% SOAP Support: Add SOAPAction header to the message Request2 = case lists:keysearch(soap,#xmlElement.name, Element#xmlElement.content) of {value, SoapEl=#xmlElement{} } -> SOAPAction = ts_config:getAttr(SoapEl#xmlElement.attributes, action), Request#http_request{soap_action=SOAPAction}; _ -> Request end, PreviousHTTPServer = get_previous_http_server(Tab, CurS#session.id), %% client side cookies Cookies = parse_cookie(Element#xmlElement.content, []), %% Custom HTTP headers Headers= parse_headers(Element#xmlElement.content, Request2#http_request.headers), Request3 = Request2#http_request{headers=Headers,cookie=Cookies}, %% HTTP Authentication Request4 = case lists:keysearch(www_authenticate, #xmlElement.name, Element#xmlElement.content) of {value, AuthEl=#xmlElement{} } -> UserId= ts_config:getAttr(string,AuthEl#xmlElement.attributes, userid, undefined), Type = ts_config:getAttr(string,AuthEl#xmlElement.attributes, type, "basic"), Nonce = ts_config:getAttr(string,AuthEl#xmlElement.attributes, nonce, undefined), Opaque = ts_config:getAttr(string,AuthEl#xmlElement.attributes, opaque, undefined), Cnonce = ts_config:getAttr(string,AuthEl#xmlElement.attributes, cnonce, "%%ts_user_server:get_really_unique_id%%"), Nc = ts_config:getAttr(string,AuthEl#xmlElement.attributes, nc, "00000001"), Passwd= ts_config:getAttr(string,AuthEl#xmlElement.attributes, passwd, undefined), Realm = ts_config:getAttr(string,AuthEl#xmlElement.attributes, realm, undefined), QOP = ts_config:getAttr(string,AuthEl#xmlElement.attributes, qop, undefined), ?DebugF("DIGEST ? : ~p ~p ~p", [Type, Nonce, Realm]), Request3#http_request{userid=UserId, passwd=Passwd, auth_type=Type, digest_nonce=Nonce, digest_cnonce=Cnonce, digest_nc=Nc, digest_opaque=Opaque, realm=Realm, digest_qop=QOP}; _ -> Request3 end, %% OAuth Msg = case lists:keysearch(oauth, #xmlElement.name, Element#xmlElement.content) of {value, AuthEl2=#xmlElement{} } -> ConsumerKey = ts_config:getAttr(string,AuthEl2#xmlElement.attributes, consumer_key, undefined), ConsumerSecret = ts_config:getAttr(string,AuthEl2#xmlElement.attributes, consumer_secret, undefined), AccessToken = ts_config:getAttr(string,AuthEl2#xmlElement.attributes, access_token, []), AccessTokenSecret = ts_config:getAttr(string,AuthEl2#xmlElement.attributes, access_token_secret, []), Encoding = ts_config:getAttr(string,AuthEl2#xmlElement.attributes, encoding, "HMAC-SHA1"), AbsoluteURL = case URL of "http" ++ _Rest -> hd(string:tokens(URL, "?")); _Relative -> server_to_url(Server) ++ hd(string:tokens(URL, "?")) end, SigEncoding = case Encoding of "PLAINTEXT" -> plaintext; "HMAC-SHA1" -> hmac_sha1; "RSA-SHA1" -> rsa_sha1 end, NewReq=Request4#http_request{oauth_access_token=AccessToken, oauth_url=AbsoluteURL, oauth_access_secret=AccessTokenSecret, oauth_consumer={ConsumerKey, ConsumerSecret, SigEncoding}}, set_msg(NewReq, {SubstFlag, MatchRegExp, UseProxy, Servers, PreviousHTTPServer, Tab, CurS#session.id} ); _ -> set_msg(Request4, {SubstFlag, MatchRegExp, UseProxy, Servers, PreviousHTTPServer, Tab, CurS#session.id} ) end, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id},Msg#ts_request{endpage=true, dynvar_specs=DynVar}}), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing default values parse_config(Element = #xmlElement{name=option}, Conf = #config{session_tab = Tab}) -> case ts_config:getAttr(Element#xmlElement.attributes, name) of "user_agent" -> lists:foldl( fun(A,B)->parse_config(A,B) end, Conf, Element#xmlElement.content); "http_use_server_as_proxy" -> Val = ts_config:getAttr(Element#xmlElement.attributes, value), ets:insert(Tab,{{http_use_server_as_proxy}, Val}) end, lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Conf, Element#xmlElement.content); %% Parsing user_agent parse_config(Element = #xmlElement{name=user_agent}, Conf = #config{session_tab = Tab}) -> Proba = ts_config:getAttr(integer,Element#xmlElement.attributes, probability), ValRaw = ts_config:getText(Element#xmlElement.content), Val = ts_utils:clean_str(ValRaw), ?LOGF("Get user agent: ~p ~p ~n",[Proba, Val],?WARN), Previous = case ets:lookup(Tab, {http_user_agent, value}) of [] -> []; [{_Key,Old}] -> Old end, ets:insert(Tab,{{http_user_agent, value}, [{Proba, Val}|Previous]}), lists:foldl( fun(A,B)->parse_config(A,B) end, Conf, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. get_previous_http_server(Ets, Id) -> case ets:lookup(Ets, {http_server, Id}) of [] -> []; [{_Key,PrevServ}] -> PrevServ end. %%---------------------------------------------------------------------- %% Func: parse_headers/2 %% Args: Elements (list), Headers (list) %% Returns: List %% Purpose: parse http_header elements %%---------------------------------------------------------------------- parse_headers([], Headers) -> Headers; parse_headers([Element = #xmlElement{name=http_header} | Tail], Headers) -> Name = ts_config:getAttr(string, Element#xmlElement.attributes, name), Value = ts_config:getAttr(string, Element#xmlElement.attributes, value), EncodedValue = case ts_config:getAttr(atom, Element#xmlElement.attributes, encoding, none) of base64 -> ts_utils:encode_base64(Value); none -> Value end, parse_headers(Tail, [{Name, EncodedValue} | Headers]); parse_headers([_| Tail], Headers) -> parse_headers(Tail, Headers). parse_cookie([], Cookies) -> Cookies; parse_cookie([Element = #xmlElement{name=add_cookie} | Tail], Cookies) -> Key = ts_config:getAttr(string, Element#xmlElement.attributes, key), Value = ts_config:getAttr(string, Element#xmlElement.attributes, value), Path = ts_config:getAttr(string, Element#xmlElement.attributes, path,"/"), Domain= ts_config:getAttr(string,Element#xmlElement.attributes,domain,"."), parse_cookie(Tail,[#cookie{key=Key,value=Value,path=Path,domain=Domain}|Cookies]); parse_cookie([_| Tail], Cookies) -> parse_cookie(Tail, Cookies). %%---------------------------------------------------------------------- %% Func: set_msg/2 %% Returns: #ts_request record %% Purpose: build the #ts_request record from an #http_request, %% and Substitution def. %%---------------------------------------------------------------------- %% if the URL is full (http://...), we parse it and get server host, %% port and scheme from the URL and override the global setup of the %% server. These information are stored in the #ts_request record. set_msg(HTTP=#http_request{url="http" ++ URL}, {SubstFlag, MatchRegExp, UseProxy, [Server|_], _PrevHTTPServer, Tab, Id}) -> case {SubstFlag, re:run(URL, "%%.+%%")} of {true, {match,_}} -> %% url is a dynvar, don't preset host header set_msg2(HTTP, #ts_request{ack = parse, subst = true, match = MatchRegExp }); _ -> URLrec = parse_URL("http" ++ URL), Path = set_query(URLrec), HostHeader = set_host_header(URLrec), Port = set_port(URLrec), Scheme = set_scheme({URLrec#url.scheme,Server#server.type}), ets:insert(Tab,{{http_server, Id}, {HostHeader, URLrec#url.scheme}}), {RealServer, RealPath} = case UseProxy of true -> {Server, "http"++ URL}; false -> {#server{host=URLrec#url.host,port=Port,type=Scheme},Path} end, set_msg2(HTTP#http_request{url=RealPath, host_header = HostHeader, use_proxy=UseProxy}, #ts_request{ack = parse, subst = SubstFlag, match = MatchRegExp, host = RealServer#server.host, scheme = RealServer#server.type, port = RealServer#server.port}) end; %% relative URL, no previous HTTP server, use proxy, error ! set_msg(_, {_, _, true, _Server, [],_Tab,_Id}) -> ?LOG("Need absolute URL when using a proxy ! Abort",?ERR), throw({error, badurl_proxy}); %% url head is a dynvar, don't preset host header set_msg(HTTP=#http_request{url="%%" ++ _TailURL}, {true, MatchRegExp, _Proxy, _Servers, _Headers,_Tab,_Id}) -> set_msg2(HTTP, #ts_request{ack = parse, subst = true, match = MatchRegExp }); %% relative URL, no proxy, a single server => we can preset host header at configuration time set_msg(HTTPRequest, Args={_SubstFlag, _MatchRegExp, false, [Server], [],_Tab,_Id}) -> ?LOG("Relative URL, single server ",?INFO), URL = server_to_url(Server) ++ HTTPRequest#http_request.url, set_msg(HTTPRequest#http_request{url=URL}, Args); %% relative URL, no proxy, several servers: don't set host header %% since the real server will be choose at run time set_msg(HTTPRequest, {SubstFlag, MatchRegExp, false, _Servers, [],_Tab,_Id}) -> set_msg2(HTTPRequest, #ts_request{ack = parse, subst = SubstFlag, match = MatchRegExp }); %% relative URL, no proxy set_msg(HTTPRequest, {SubstFlag, MatchRegExp, false, _Server, {HostHeader,_},_Tab,_Id}) -> set_msg2(HTTPRequest#http_request{host_header= HostHeader}, #ts_request{ack = parse, subst = SubstFlag, match = MatchRegExp }); %% relative URL, use proxy set_msg(HTTPRequest, {SubstFlag, MatchRegExp, true, Server, {HostHeader, PrevScheme},Tab,Id}) -> %% set absolute URL using previous Server used. URL = atom_to_list(PrevScheme) ++ "://" ++ HostHeader ++ HTTPRequest#http_request.url, set_msg(HTTPRequest#http_request{url=URL}, {SubstFlag, MatchRegExp, true, Server, {HostHeader, PrevScheme},Tab,Id}). %% Func: set_mgs2/3 %% Purpose: set param in ts_request set_msg2(HTTPRequest, Msg) -> Msg#ts_request{ param = HTTPRequest }. server_to_url(#server{port=443, host= Host, type= Type}) when Type==ts_ssl orelse Type==ts_ssl6-> "https://" ++ encode_ipv6_address(Host); server_to_url(#server{port=Port, host= Host, type= Type})when Type==ts_ssl orelse Type==ts_ssl6-> "https://" ++ encode_ipv6_address(Host) ++ ":" ++ integer_to_list(Port); server_to_url(#server{port=80, host= Host})-> "http://" ++ encode_ipv6_address(Host); server_to_url(#server{port=Port, host= Host})-> "http://" ++ encode_ipv6_address(Host) ++ ":" ++ integer_to_list(Port). %%-------------------------------------------------------------------- %% Func: set_host_header/1 %%-------------------------------------------------------------------- %% if port is undefined, don't need to set port, because it use the default (80 or 443) set_host_header(#url{host=Host,port=undefined}) -> encode_ipv6_address(Host); set_host_header(#url{host=Host,port=Port}) when is_integer(Port) -> encode_ipv6_address(Host) ++ ":" ++ integer_to_list(Port). encode_ipv6_address(Host) when hd(Host)==$[ -> %% ipv6; already using [] (rfc2732), no need to add Host; encode_ipv6_address(Host)-> case string:chr(Host,$:) of 0 -> Host; % regular name or ipv4 address _ -> "["++Host++"]" end. %%-------------------------------------------------------------------- %% Func: set_port/1 %% Purpose: Returns port according to scheme if not already defined %% Returns: PortNumber (integer) %%-------------------------------------------------------------------- set_port(#url{scheme=https,port=undefined}) -> 443; set_port(#url{scheme=http,port=undefined}) -> 80; set_port(#url{port=Port}) when is_integer(Port) -> Port; set_port(#url{port=Port}) -> integer_to_list(Port). %% @spec set_scheme({http|https,gen_tcp|gen_tcp6|ssl|ssl6})-> gen_tcp|gen_tcp6|ssl|ssl6 %% @doc set scheme for given protocol and server setup. If the main %% server is configured with IPv6, we assume that the we should also %% use IPv6 for the given absolute URL %% @end set_scheme({http, ts_tcp6}) -> ts_tcp6; set_scheme({http, ts_ssl6}) -> ts_tcp6; set_scheme({http, _}) -> ts_tcp; set_scheme({https, ts_ssl6}) -> ts_ssl6; set_scheme({https, ts_tcp6}) -> ts_ssl6; set_scheme({https, _}) -> ts_ssl. set_query(URLrec = #url{querypart=""}) -> URLrec#url.path; set_query(URLrec = #url{}) -> URLrec#url.path ++ "?" ++ URLrec#url.querypart. %%---------------------------------------------------------------------- %% Func: parse_URL/1 %% Returns: #url %%---------------------------------------------------------------------- parse_URL("https://" ++ String) -> parse_URL(host, String, [], #url{scheme=https}); parse_URL("http://" ++ String) -> parse_URL(host, String, [], #url{scheme=http}); parse_URL(String) when is_list(String) -> case string:tokens(String,":") of [Host, Port] -> #url{scheme=connect, host=Host, port=list_to_integer(Port)}; RelativeURL when is_list(RelativeURL) -> parse_URL(path, RelativeURL, [], #url{scheme=http}) end. %%---------------------------------------------------------------------- %% Func: parse_URL/4 (inspired by yaws_api.erl) %% Returns: #url record %%---------------------------------------------------------------------- % parse host parse_URL(host, [$[|Tail] , [], URL) -> % host starts with '[': ipv6 address in url {match,[Host,Rest]} = re:run(Tail,"^([^\\]]*)\\](.*)",[{capture,all_but_first,list}]), parse_URL(host, Rest, lists:reverse(Host), URL); parse_URL(host, [], Acc, URL) -> % no path or port URL#url{host=lists:reverse(Acc), path= "/"}; parse_URL(host, [$/|Tail], Acc, URL) -> % path starts here parse_URL(path, Tail, "/", URL#url{host=lists:reverse(Acc)}); parse_URL(host, [$?|Tail], Acc, URL) -> % path/query starts here parse_URL(path, "?" ++ Tail, "/", URL#url{host=lists:reverse(Acc)}); parse_URL(host, [$:|Tail], Acc, URL) -> % port starts here parse_URL(port, Tail, [], URL#url{host=lists:reverse(Acc)}); parse_URL(host, [H|Tail], Acc, URL) -> parse_URL(host, Tail, [H|Acc], URL); % parse port parse_URL(port,[], Acc, URL) -> URL#url{port=list_to_integer(lists:reverse(Acc)), path= "/"}; parse_URL(port,[$/|T], Acc, URL) -> parse_URL(path, T, "/", URL#url{port=list_to_integer(lists:reverse(Acc))}); parse_URL(port,[H|T], Acc, URL) -> parse_URL(port, T, [H|Acc], URL); % parse path parse_URL(path,[], Acc, URL) -> URL#url{path=lists:reverse(Acc)}; parse_URL(path,[$?|T], Acc, URL) -> URL#url{path=lists:reverse(Acc), querypart=T}; parse_URL(path,[H|T], Acc, URL) -> parse_URL(path, T, [H|Acc], URL). % check if the sum of all user agent probabilities is equal to 100% check_user_agent_sum(Tab) -> case ets:lookup(Tab, {http_user_agent, value}) of [] -> ok; % no user agent, will use the default one. [{_Key, UserAgents}] -> ts_utils:check_sum(UserAgents, 1, ?USER_AGENT_ERROR_MSG) end. tsung-1.8.0/src/tsung_controller/ts_config_fs.erl0000644000201100017670000000632514377756736021745 0ustar nniclausdream%%% %%% Copyright 2009 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 20 août 2009 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_fs). -vc('$Id$ '). -author('nicolas.niclausse@sophia.inria.fr'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). -include("ts_fs.hrl"). %% @spec parse_config(#xmlElement{}, Config::term()) -> NewConfig::term() %% @doc Parses a tsung.xml configuration file xml element for this %% protocol and updates the Config term. %% @end parse_config(Element = #xmlElement{name=dyn_variable}, Conf = #config{}) -> ts_config:parse(Element,Conf); parse_config(Element = #xmlElement{name=fs}, Config=#config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar=DynVar, subst = SubstFlag, match=MatchRegExp}) -> Cmd = ts_config:getAttr(atom,Element#xmlElement.attributes, cmd, write), Size = ts_config:getAttr(integer,Element#xmlElement.attributes, size, 1024), Path = ts_config:getAttr(string,Element#xmlElement.attributes, path), Mode = ts_config:getAttr(atom,Element#xmlElement.attributes, mode, write), Dest = ts_config:getAttr(string,Element#xmlElement.attributes, dest), Position = ts_config:getAttr(integer,Element#xmlElement.attributes, position, undefined), Request = #fs{command=Cmd,size=Size,mode=Mode,path=Path,position=Position, dest=Dest}, Msg= #ts_request{ack = parse, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ts_config:mark_prev_req(Id-1, Tab, CurS), ets:insert(Tab,{{CurS#session.id, Id},Msg}), lists:foldl( fun(A,B)->ts_config:parse(A,B) end, Config#config{dynvar=[]}, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. tsung-1.8.0/src/tsung_controller/ts_config.erl0000644000201100017670000020476314377756736021263 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2003 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%---------------------------------------------------------------------- %%% @author Nicolas Niclausse %%% @todo learn how to use xmerl correctly %%% @doc Read the tsung XML config file. Currently, it %%% work by parsing the #xmlElement record by hand ! %%% @end %%% Created : 3 Dec 2003 by Nicolas Niclausse %%%---------------------------------------------------------------------- -module(ts_config). -author('nicolas@niclux.org'). -vc('$Id$ '). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("xmerl.hrl"). -export([read/2, getAttr/2, getAttr/3, getAttr/4, getText/1, parse/2, get_default/2, get_default/3, mark_prev_req/3, get_batch_nodes/1 ]). %%%---------------------------------------------------------------------- %%% @spec: read(Filename::string(), LogDir::string()) -> %%% {ok, Config::#config{} } | {error, Reason::term()} %%% @doc: read and parse the xml config file %%% @end %%%---------------------------------------------------------------------- read(Filename=standard_io, LogDir) -> ?LOG("Reading config file from stdin~n", ?NOTICE), XML = read_stdio(), handle_read(catch xmerl_scan:string(XML, [{fetch_path,["/usr/share/tsung/","./"]}, {validation,true}]),Filename,LogDir); read(Filename, LogDir) -> ?LOGF("Reading config file: ~s~n", [Filename], ?NOTICE), Result = handle_read(catch xmerl_scan:file(Filename, [{fetch_path,["/usr/share/tsung/","./"]}, {validation,true}]),Filename,LogDir), %% In case of error we reparse the file with xmerl_sax_parser:file/2 to obtain %% a more verbose output case Result of {ok, _} -> Result; _ -> xmerl_sax_parser:file(Filename,[]), Result end. handle_read( {Root = #xmlElement{}, _Tail}, Filename, LogDir) -> Table = ets:new(sessiontable, [ordered_set, protected]), backup_config(LogDir, Filename, Root), {ok, parse(Root, #config{session_tab = Table, proto_opts=#proto_opts{}})}; handle_read({error,Reason},_,_) -> {error, Reason}; handle_read({'EXIT',Reason},_,_) -> {error, Reason}. %%%---------------------------------------------------------------------- %%% Function: parse/2 %%% Purpose: parse the xmerl structure %%%---------------------------------------------------------------------- parse(Element = #xmlElement{parents = [], attributes=Attrs}, Conf=#config{}) -> Loglevel = getAttr(string, Attrs, loglevel, "notice"), Dump = getAttr(string, Attrs, dumptraffic, "false"), BackEnd = getAttr(atom, Attrs, backend, text), DumpType = case Dump of "false" -> none; "true" -> full; "light" -> light; "protocol" -> protocol; "protocol_local" -> protocol_local end, lists:foldl(fun parse/2, Conf#config{dump= DumpType, stats_backend=BackEnd, loglevel= ts_utils:level2int(Loglevel)}, Element#xmlElement.content); %% parsing the Server elements parse(Element = #xmlElement{name=server, attributes=Attrs}, Conf=#config{servers=ServerList, total_server_weights=OldTotal}) -> Server = getAttr(Attrs, host), Port = getAttr(integer, Attrs, port), Weight = getAttr(float_or_integer, Attrs, weight,1), Type = set_net_type(getAttr(Attrs, type)), Total = OldTotal + Weight, lists:foldl(fun parse/2, Conf#config{servers = [#server{host = Server, port = Port, weight= Weight, type = Type }|ServerList], total_server_weights = Total}, Element#xmlElement.content); %% Parsing the cluster monitoring element (monitor) parse(Element = #xmlElement{name=monitor, attributes=Attrs}, Conf = #config{monitor_hosts=MHList}) -> Host = getAttr(Attrs, host), Type = case getAttr(atom, Attrs, type, erlang) of erlang -> case lists:keysearch(mysqladmin,#xmlElement.name, Element#xmlElement.content) of {value, MysqlEl=#xmlElement{} } -> Port = getAttr(integer,MysqlEl#xmlElement.attributes, port, ?config(mysql_port)), Username = getAttr(string,MysqlEl#xmlElement.attributes, username, ?config(mysql_user)), Password = getAttr(string,MysqlEl#xmlElement.attributes, password, ?config(mysql_password)), {erlang, [{mysqladmin, {Port, Username, Password}}]}; _ -> {erlang, []} end; snmp -> case lists:keysearch(snmp,#xmlElement.name, Element#xmlElement.content) of {value, SnmpEl=#xmlElement{} } -> Port = getAttr(integer,SnmpEl#xmlElement.attributes, port, ?config(snmp_port)), Community = getAttr(string,SnmpEl#xmlElement.attributes, community, ?config(snmp_community)), Version = getAttr(atom,SnmpEl#xmlElement.attributes, version, ?config(snmp_version)), %% parse OIDS def TmpConf = lists:foldl(fun parse/2, Conf#config{oids=[]}, SnmpEl#xmlElement.content), {snmp, {Port, Community, Version,TmpConf#config.oids}}; _ -> {snmp, {?config(snmp_port), ?config(snmp_community), ?config(snmp_version),[]}} end; munin -> case lists:keysearch(munin,#xmlElement.name, Element#xmlElement.content) of {value, MuninEl=#xmlElement{} } -> Port = getAttr(integer,MuninEl#xmlElement.attributes, port, ?config(munin_port)), {munin, {Port}}; _ -> {munin, {?config(munin_port) }} end end, NewMon = case getAttr(atom, Attrs, batch, false) of true -> Nodes = lists:usort(get_batch_nodes(list_to_atom(Host))), lists:map(fun(N)-> {N, Type} end, Nodes); _ -> [{Host, Type}] end, lists:foldl(fun parse/2, Conf#config{monitor_hosts = lists:append(MHList, NewMon)}, Element#xmlElement.content); parse(#xmlElement{name=oid, attributes=Attrs}, Conf=#config{oids=OIDS}) -> OIDStr = getAttr(Attrs, value), OID = lists:map(fun erlang:list_to_integer/1, string:tokens(OIDStr,".")), Name = getAttr(atom, Attrs, name), Type = case getAttr(atom, Attrs, type, sample) of sample -> sample; counter -> sample_counter; sum -> sum end, Snippet = getAttr(string, Attrs, eval, "fun(X)-> X end."), Fun= ts_utils:eval(Snippet), true = is_function(Fun, 1), Conf#config{oids=[{OID,Name,Type,Fun}| OIDS]}; %% parse(Element = #xmlElement{name=load, attributes=Attrs}, Conf) -> Loop = getAttr(integer, Attrs, loop, 0), IDuration = getAttr(integer, Attrs, duration, 0), Unit = getAttr(string, Attrs, unit, "second"), Duration = to_seconds(Unit, IDuration), lists:foldl(fun parse/2, Conf#config{load_loop=Loop,duration=Duration}, Element#xmlElement.content); %% Parsing the Client element parse(Element = #xmlElement{name=client, attributes=Attrs}, Conf = #config{clients=CList}) -> Host = getAttr(Attrs, host), Weight = getAttr(integer,Attrs, weight,1), MaxUsers = getAttr(integer,Attrs, maxusers, 800), SingleNode = getAttr(atom, Attrs, use_controller_vm, false) or Conf#config.use_controller_vm, NewClients = case getAttr(atom, Attrs, type) of batch -> ?LOG("Get client nodes from batch scheduler~n",?DEB), Batch = getAttr(atom, Attrs, batch), Scan_Intf = getAttr(Attrs, scan_intf), NodesTmp = get_batch_nodes(Batch), case NodesTmp of []-> ?LOGF("Warning: empty list of nodes from batch: ~p~n",[NodesTmp],?WARN); _ -> ?LOGF("nodes: ~p~n",[NodesTmp],?DEB) end, %% remove controller host from list to avoid %% overloading the machine running the controller {ok, ControllerHost} = ts_utils:node_to_hostname(node()), DeleteController=fun(A) when A == ControllerHost -> false; (_) -> true end, Nodes = case lists:filter(DeleteController, NodesTmp) of [] -> NodesTmp; %% all nodes are on the controller, don't remove them Val -> Val end, Fun = fun(N)-> IP = case Scan_Intf of "" -> []; Interface -> case os:type() of {unix, linux} -> [{scan, Interface}]; OS -> io:format(standard_error,"Scan interface is not supported on OS ~p, abort~n",[OS]), exit({error, scan_interface_not_supported_on_os}) end end, #client{host=N,weight=Weight,ip=IP,maxusers=MaxUsers} end, lists:map(Fun, Nodes); _ -> CPU = case {getAttr(integer,Attrs, cpu, 1), SingleNode} of {Val, true} when Val > 1 -> erlang:display("Can't use CPU > 1 when use_controller_vm is true ! Set CPU to 1."), 1; {Val, _} -> Val end, %% if the node()'s hostname is ip, then all host should be IP {ok, MasterHostname} = ts_utils:node_to_hostname(node()), case {ts_utils:is_ip(MasterHostname), ts_utils:is_ip(Host)} of %% must be hostname and not ip: {false, true} -> io:format(standard_error,"ERROR: client config: 'host' attribute must be a hostname, "++ "not an IP ! (was ~p). You can use -I <> option.~n",[Host]), exit({error, badhostname}); {true, true} -> %% add a new client for each CPU lists:duplicate(CPU,#client{host = Host, weight = Weight/CPU, maxusers = MaxUsers}); {_, _} -> %% add a new client for each CPU lists:duplicate(CPU,#client{host = Host, weight = Weight/CPU, maxusers = MaxUsers}) end end, lists:foldl(fun parse/2, Conf#config{clients = lists:append(NewClients,CList), use_controller_vm = SingleNode}, Element#xmlElement.content); %% Parsing the ip element parse(Element = #xmlElement{name=ip, attributes=Attrs}, Conf = #config{clients=[CurClient|CList]}) -> IPList = CurClient#client.ip, IP = case getAttr(atom, Attrs, scan, false) of true -> {scan, getAttr(string,Attrs, value, "eth0")}; _ -> ToResolve = case getAttr(Attrs, value) of "resolve" -> CurClient#client.host; StrIP -> StrIP end, ?LOGF("resolving host ~p~n",[ToResolve],?WARN), {ok,IPtmp} = case inet:getaddr(ToResolve,inet) of {error,nxdomain} -> % retry with IPv6 inet:getaddr(ToResolve,inet6); Val -> Val end, IPtmp end, ?LOGF("resolved host ~p~n",[IP],?WARN), lists:foldl(fun parse/2, Conf#config{clients = [CurClient#client{ip = [IP|IPList]} |CList]}, Element#xmlElement.content); %% Parsing the iprange parse(Element = #xmlElement{name=iprange, attributes=Attrs}, Conf = #config{clients=[CurClient|CList]}) -> %% only ipv4 currently IP = getAttr(Attrs, value), SubList = string:tokens(IP, "."), [A,B,C,D] = lists:map(fun(A) -> case getTypeAttr(integer_or_string, A) of I when is_integer(I) -> I; S when is_list(S) -> [Min, Max] = lists:map(fun(X)-> list_to_integer(X) end, string:tokens(S,"-")), {Min, Max} end end, SubList), ?LOGF("IP range: ~p~n",[ { A,B,C,D }],?INFO), lists:foldl(fun parse/2, Conf#config{clients = [CurClient#client{iprange = {A,B,C,D} } | CList]}, Element#xmlElement.content); %% Parsing the arrivalphase element parse(Element = #xmlElement{name=arrivalphase, attributes=Attrs}, Conf = #config{arrivalphases=AList}) -> Phase = getAttr(integer, Attrs, phase), IDuration = getAttr(integer, Attrs, duration), Unit = getAttr(string, Attrs, unit, "second"), WaitSessionsEnd = getAttr(atom,Attrs, wait_all_sessions_end, false), D = to_milliseconds(Unit, IDuration), case lists:keysearch(Phase,#arrivalphase.phase,AList) of false -> lists:foldl(fun parse/2, Conf#config{arrivalphases = [#arrivalphase{phase=Phase, wait_all_sessions_end=WaitSessionsEnd, duration=D } |AList]}, Element#xmlElement.content); _ -> % already existing phase, wrong configuration. io:format(standard_error,"Client config error: phase ~p already defined, abort !~n",[Phase]), exit({error, already_defined_phase}) end; %% Parsing the user element parse(Element = #xmlElement{name=user, attributes=Attrs}, Conf = #config{static_users=Users}) -> Start = getAttr(float_or_integer,Attrs, start_time), Unit = getAttr(string,Attrs, unit, "second"), Session = getAttr(string,Attrs, session), Delay = to_milliseconds(Unit,Start), NewUsers= Users++[{Delay,Session}], lists:foldl(fun parse/2, Conf#config{static_users = NewUsers}, Element#xmlElement.content); %% Parsing the users element parse(Element = #xmlElement{name=users, attributes=Attrs}, Conf = #config{arrivalphases=[CurA | AList]}) -> Max = getAttr(integer,Attrs, maxnumber, infinity), ?LOGF("Maximum number of users ~p~n",[Max],?INFO), Unit = getAttr(string,Attrs, unit, "second"), Intensity = case {getAttr(float_or_integer,Attrs, interarrival), getAttr(float_or_integer,Attrs, arrivalrate) } of {[],[]} -> exit({invalid_xml,"arrival or interarrival must be specified"}); {[], Rate} when Rate > 0 -> Rate / to_milliseconds(Unit,1); {InterArrival,[]} when InterArrival > 0 -> 1/to_milliseconds(Unit,InterArrival); {_Value, _Value2} -> exit({invalid_xml,"arrivalrate and interarrival can't be defined simultaneously"}) end, lists:foldl(fun parse/2, Conf#config{arrivalphases = [CurA#arrivalphase{maxnumber = Max, intensity=Intensity} |AList]}, Element#xmlElement.content); %% Parsing the session element parse(Element = #xmlElement{name=session, attributes=Attrs}, Conf = #config{curid= PrevReqId, sessions=SList}) -> Id = length(SList), Type = getAttr(atom,Attrs, type), {Persistent_def, Bidi_def} = case Type:session_defaults() of {ok, Pdef, Bdef} -> {Pdef, Bdef}; {ok, Pdef} -> {Pdef, false} end, Persistent = getAttr(atom,Attrs, persistent, Persistent_def), Bidi = getAttr(atom,Attrs, bidi, Bidi_def), Name = getAttr(Attrs, name), ?LOGF("Session name for id ~p is ~p~n",[Id+1, Name],?NOTICE), ?LOGF("Session type: persistent=~p, bidi=~p~n",[Persistent,Bidi],?INFO), Probability = getAttr(float_or_integer, Attrs, probability, -1), Weight = getAttr(float_or_integer, Attrs, weight, -1), {Popularity, NewUseWeights, NewTotal} = get_popularity(Probability, Weight, Conf#config.use_weights,Conf#config.total_popularity), NewSList = case SList of [] -> []; % first session [Previous|Tail] -> % set total requests count in previous session [Previous#session{size=PrevReqId,type=Conf#config.main_sess_type}|Tail] end, lists:foldl(fun parse/2, Conf#config{sessions = [#session{id = Id + 1, popularity = Popularity, type = Type, name = Name, persistent = Persistent, bidi = Bidi, rate_limit = Conf#config.rate_limit, hibernate = Conf#config.hibernate, proto_opts = Conf#config.proto_opts } |NewSList], main_sess_type = Type, use_weights=NewUseWeights, total_popularity=NewTotal, curid=0, cur_req_id=0},% re-initialize request id Element#xmlElement.content); %% Parsing the session_setup element parse(Element = #xmlElement{name=session_setup, attributes=Attrs}, Conf = #config{arrivalphases=[Phase|Phases]}) -> Name = getAttr(Attrs, name), {Popularity, NewUseWeights} = case { Conf#config.use_weights, getAttr(float_or_integer, Attrs, weight, -1) } of {Use, -1} when (Use == undefined orelse Use == false) -> { getAttr(float_or_integer, Attrs, probability, -1), false }; {_, Val} -> { Val, true} end, SessionsPopularities = Phase#arrivalphase.popularities, NewPhase = Phase#arrivalphase{popularities= [{Name, Popularity}| SessionsPopularities]}, lists:foldl(fun parse/2, Conf#config{ use_weights = NewUseWeights, arrivalphases = [NewPhase | Phases] }, Element#xmlElement.content); %%%% Parsing the transaction element parse(Element = #xmlElement{name=transaction, attributes=Attrs}, Conf = #config{session_tab = Tab, sessions=[CurS|_], curid=Id}) -> RawName = getAttr(Attrs, name), {ok, [{atom,_,Name}],_} = erl_scan:string("tr_"++RawName), ?LOGF("Add start transaction ~p in session ~p as id ~p", [Name,CurS#session.id,Id+1],?INFO), ets:insert(Tab, {{CurS#session.id, Id+1}, {transaction,start,Name}}), NewConf=lists:foldl( fun parse/2, Conf#config{curid=Id+1}, Element#xmlElement.content), NewId = NewConf#config.curid, ?LOGF("Add end transaction ~p in session ~p as id ~p", [Name,CurS#session.id,NewId+1],?INFO), ets:insert(Tab, {{CurS#session.id, NewId+1}, {transaction,stop,Name}}), NewConf#config{curid=NewId+1} ; %%%% Parsing the 'if' element parse(_Element = #xmlElement{name='if', attributes=Attrs,content=Content}, Conf = #config{session_tab = Tab, sessions=[CurS|_], curid=Id}) -> VarName=get_dynvar_name(getAttr(string,Attrs,var)), {Rel,Value} = case {getAttr(string,Attrs,eq,none), getAttr(string,Attrs,neq,none), getAttr(string,Attrs,gt,none), getAttr(string,Attrs,lt,none), getAttr(string,Attrs,gte,none), getAttr(string,Attrs,lte,none)} of {none, Neq, none, none, none, none} -> {neq,Neq}; {Eq, none, none, none, none, none} -> {eq,Eq}; {none, none, Gt, none, none, none} -> {gt,Gt}; {none, none, none, Lt, none, none} -> {lt,Lt}; {none, none, none, none, Gte, none} -> {gte,Gte}; {none, none, none, none, none, Lte} -> {lte,Lte} end, ?LOGF("Add if_start action in session ~p as id ~p", [CurS#session.id,Id+1],?INFO), NewConf = lists:foldl(fun parse/2, Conf#config{curid=Id+1}, Content), NewId = NewConf#config.curid, ?LOGF("endif in session ~p as id ~p",[CurS#session.id,NewId+1],?INFO), SubstitutionFlag = case re:run(Value, "%%.+%%") of {match, _} -> subst; nomatch -> nosubst end, InitialAction = {ctrl_struct, {if_start, Rel, VarName, list_to_binary(Value), SubstitutionFlag, NewId+1}}, %%NewId+1 -> id of the first action after the if ets:insert(Tab,{{CurS#session.id,Id+1},InitialAction}), NewConf; %%%% Parsing the 'for' element parse(_Element = #xmlElement{name=for, attributes=Attrs,content=Content}, Conf = #config{session_tab = Tab, sessions=[CurS|_], curid=Id}) -> VarName = getAttr(atom,Attrs,var), InitValue = getAttr(integer_or_string,Attrs,from), EndValue = getAttr(integer_or_string,Attrs,to), Increment = getAttr(integer,Attrs,incr,1), InitialAction = {ctrl_struct, {for_start, InitValue, VarName}}, ?LOGF("Add for_start action in session ~p as id ~p", [CurS#session.id,Id+1],?INFO), ets:insert(Tab,{{CurS#session.id,Id+1},InitialAction}), NewConf = lists:foldl(fun parse/2, Conf#config{curid=Id+1}, Content), NewId = NewConf#config.curid, EndAction= {ctrl_struct,{for_end,VarName,EndValue,Increment,Id+2}}, %%Id+2 -> id of the first action inside the loop %% (id+1 is the for_start action) ?LOGF("Add for_end action in session ~p as id ~p, Jump to:~p", [CurS#session.id,NewId+1,Id+2],?INFO), ets:insert(Tab, {{CurS#session.id,NewId+1},EndAction}), NewConf#config{curid=NewId+1}; %%%% Parsing the 'foreach' element parse(_Element = #xmlElement{name=foreach, attributes=Attrs,content=Content}, Conf = #config{session_tab = Tab, sessions=[CurS|_], curid=Id}) -> VarName = getAttr(atom,Attrs,in), ForName = getAttr(atom,Attrs,name), Filter = case getAttr(string,Attrs,include) of "" -> case getAttr(string,Attrs,exclude) of "" -> undefined; Re2 -> {ok, RegExp} = re:compile(Re2), {false,RegExp} end; Re -> {ok, CompiledRegExp} = re:compile(Re), {true,CompiledRegExp} end, InitialAction = {ctrl_struct, {foreach_start, ForName, VarName, Filter}}, ?LOGF("Add foreach_start action in session ~p as id ~p", [CurS#session.id,Id+1],?INFO), ets:insert(Tab,{{CurS#session.id,Id+1},InitialAction}), NewConf = lists:foldl(fun parse/2, Conf#config{curid=Id+1}, Content), NewId = NewConf#config.curid, EndAction= {ctrl_struct,{foreach_end,ForName,VarName,Filter,Id+2}}, %%Id+2 -> id of the first action inside the loop %% (id+1 is the foreach_start action) ?LOGF("Add foreach_end action in session ~p as id ~p, Jump to:~p", [CurS#session.id,NewId+1,Id+2],?INFO), ets:insert(Tab, {{CurS#session.id,NewId+1},EndAction}), NewConf#config{curid=NewId+1}; %%%% Parsing the 'repeat' element %%%% Last child element must be either 'while' or 'until' parse(_Element = #xmlElement{name=repeat,attributes=Attrs,content=Content}, Conf = #config{session_tab = Tab, sessions=[CurS|_], curid=Id}) -> MaxRepeat = getAttr(integer,Attrs,max_repeat,20), RepeatName = get_dynvar_name(getAttr(string,Attrs,name)), [LastElement|_] = lists:reverse([E || E=#xmlElement{} <- Content]), case LastElement of #xmlElement{name=While,attributes=WhileAttrs} when (While == 'while') or (While == 'until')-> {Rel,Value} = case {getAttr(string,WhileAttrs,eq,none), getAttr(string,WhileAttrs,neq,none), getAttr(string,WhileAttrs,gt,none), getAttr(string,WhileAttrs,lt,none), getAttr(string,WhileAttrs,gte,none), getAttr(string,WhileAttrs,lte,none)} of {none, Neq, none, none, none, none} -> {neq,Neq}; {Eq, none, none, none, none, none} -> {eq,Eq}; {none, none, Gt, none, none, none} -> {gt,Gt}; {none, none, none, Lt, none, none} -> {lt,Lt}; {none, none, none, none, Gte, none} -> {gte,Gte}; {none, none, none, none, none, Lte} -> {lte,Lte} end, %either , %either , %either , Var = getAttr(atom,WhileAttrs,var), NewConf = lists:foldl(fun parse/2, Conf#config{curid=Id}, Content), NewId = NewConf#config.curid, EndAction = {ctrl_struct,{repeat,RepeatName, While,Rel,Var,list_to_binary(Value),Id+1, MaxRepeat}}, %Id+1 -> id of the first action inside the loop ?LOGF("Add repeat action in session ~p as id ~p, Jump to: ~p", [CurS#session.id,NewId+1,Id+1],?INFO), ets:insert(Tab,{{CurS#session.id,NewId+1},EndAction}), NewConf#config{curid=NewId+1}; _ -> exit({invalid_xml,"Last element inside a loop must be " " or "}) end; %%% Parsing the dyn_variable element parse(#xmlElement{name=dyn_variable, attributes=Attrs}, Conf=#config{sessions=[CurS|_],dynvar=DynVars,subst=SubstitutionFlag}) -> StrName = ts_utils:clean_str(getAttr(Attrs, name)), {ok, [{atom,_,Name}],_} = erl_scan:string("'"++StrName++"'"), {Type,Expr} = case {getAttr(string,Attrs,re,none), getAttr(string,Attrs,pgsql_expr,none), getAttr(string,Attrs,xpath,none), getAttr(string,Attrs,header,none), getAttr(string,Attrs,jsonpath,none)} of {none,none,none,none,none} -> DefaultRegExp = ?DEF_RE_DYNVAR_BEGIN ++ StrName ++?DEF_RE_DYNVAR_END, {re,DefaultRegExp}; {RE,none,none,none, none} -> {re,RE}; {none,PG,none,none, none} -> {pgsql_expr,PG}; {none,none,XPath,none,none} -> {xpath,XPath}; {none,none,none,AuthHeader,none} -> {header, AuthHeader}; {none,none,none,none,JSONPath} -> {jsonpath,JSONPath} end, FlattenExpr =lists:flatten(Expr), %% precompilation of the exp DynVar = case Type of re -> ?LOGF("Add new re: ~s ~n", [Expr],?INFO), {ok, CompiledRegExp} = re:compile(FlattenExpr), %% TSUN-249 case getAttr(string,Attrs,decode,none) of "html_entities" -> ?LOGF("The re will be decoded: ~s ~n", [Expr],?INFO), {re,Name,CompiledRegExp,fun ts_utils:conv_entities/1}; _ -> {re,Name,CompiledRegExp, undefined} end; xpath -> ?LOGF("Add new xpath: ~s ~n", [Expr],?INFO), CompiledXPathExp = mochiweb_xpath:compile_xpath(FlattenExpr), {xpath,Name,CompiledXPathExp}; jsonpath -> ?LOGF("Add new jsonpath: ~s ~n", [Expr],?INFO), EnableSubstitution = case {SubstitutionFlag, re:run(Expr, "%%.+%%")} of { true, { match, _ } } -> true; _ -> false end, {jsonpath,Name,Expr,EnableSubstitution}; _Other -> ?LOGF("Add ~s ~s ~p ~n", [Type,Name,Expr],?INFO), {Type,Name,Expr} end, NewDynVar = [DynVar|DynVars], ?LOGF("Add new dyn variable=~p in session ~p", [NewDynVar, CurS#session.id],?INFO), Conf#config{ dynvar= NewDynVar }; parse( #xmlElement{name=change_type, attributes=Attrs}, Conf = #config{sessions=[CurS|Other], curid=Id,session_tab = Tab}) -> CType = getAttr(atom, Attrs, new_type), Server = getAttr(string, Attrs, host), Port = getAttr(string, Attrs, port), Store = getAttr(atom, Attrs, store, false), Restore = getAttr(atom, Attrs, restore, false), PType = set_net_type(getAttr(Attrs, server_type)), Bidi = getAttr(atom, Attrs, bidi, false), SessType=case Conf#config.main_sess_type == CType of false -> CurS#session.type; true -> CType % back to the main type end, ets:insert(Tab,{{CurS#session.id, Id+1}, {change_type, CType, Server, Port, PType, Store, Restore, Bidi}}), ?LOGF("Parse change_type (~p) ~p:~p:~p:~p (store/restore: ~p:~p)~n",[CType, Server,Port,PType,Id, Store, Restore],?NOTICE), Conf#config{main_sess_type=SessType, curid=Id+1, sessions=[CurS#session{type=CType}|Other] }; parse( #xmlElement{name=interaction, attributes=Attrs}, Conf = #config{sessions=[CurS|_Other], curid=Id,session_tab = Tab}) -> Action = list_to_atom(getAttr(string, Attrs, action, "send")), RawId = getAttr(Attrs, id), {ok, [{atom,_,IdInteraction}],_} = erl_scan:string("tr_"++RawId), ets:insert(Tab,{{CurS#session.id, Id+1}, {interaction, Action, IdInteraction}}), ?LOGF("Parse interaction ~p:~p ~n",[Action,Id],?NOTICE), Conf#config{curid=Id+1 }; parse( #xmlElement{name=abort, attributes=Attrs}, Conf = #config{sessions=[CurS|_Other], curid=Id,session_tab = Tab}) -> Type = getAttr(atom, Attrs, type, session), ets:insert(Tab,{{CurS#session.id, Id+1}, {abort, Type}}), Conf#config{curid=Id+1 }; parse( Element = #xmlElement{name=set_option, attributes=Attrs}, Conf = #config{sessions=[CurS|_Other], curid=Id,session_tab = Tab}) -> case getAttr(atom, Attrs, type) of "" -> {Type,Name,Args} = case getAttr(string, Attrs, name) of "rate_limit" -> Rate = getAttr(integer, Attrs, value), Max = getAttr(integer, Attrs, max, Rate), {undefined, rate_limit, {1024*Rate div 1000, 1024 * Max}}; "certificate" -> {value, #xmlElement{attributes=AttrCert}} = lists:keysearch(certificate, #xmlElement.name, Element#xmlElement.content), Cacert = getAttr(string, AttrCert, cacertfile, undefined), KeyFile = getAttr(string, AttrCert, keyfile, undefined), KeyPass = getAttr(string, AttrCert, keypass, undefined), CertFile = getAttr(string, AttrCert, certfile, undefined), {undefined, certificate, {Cacert, KeyFile,KeyPass,CertFile}}; "connect_timeout" -> ConnectTimeout = getAttr(integer, Attrs, value), {undefined, connect_timeout, {ConnectTimeout}} end, ets:insert(Tab,{{CurS#session.id, Id+1}, {set_option,Type,Name,Args}}), Conf#config{curid=Id+1}; Mod -> Mod:parse_config(Element, Conf) end; %%% Parsing the request element parse(Element = #xmlElement{name=request, attributes=Attrs}, Conf = #config{sessions=[CurSess|_], curid=Id}) -> Type = CurSess#session.type, SubstitutionFlag = getAttr(atom, Attrs, subst, false), Tag = getAttr(string, Attrs, tag, ""), Tags = lists:map(fun(X)->{X,ok} end, string:tokens(?config(exclude_tag),",")), %% do not add in Conf excluded requests case proplists:is_defined(Tag, Tags) of true -> ?LOGF("Tag ~p in ~p ~p ~p ~n",[Tag,true,?config(exclude_tag),Tags],?NOTICE), Conf; false -> lists:foldl( fun(A,B) ->Type:parse_config(A,B) end, Conf#config{curid=Id+1, cur_req_id=Id+1, subst=SubstitutionFlag, match=[], tag=Tag }, Element#xmlElement.content) end; %%% Match parse(Element=#xmlElement{name=match,attributes=Attrs}, Conf=#config{match=Match})-> Do = getAttr(atom, Attrs, do, continue), When = getAttr(atom, Attrs, 'when', match), Name = getAttr(string, Attrs, name, "-"), Subst = getAttr(atom, Attrs, subst, false), MaxLoop = getAttr(integer, Attrs, max_loop, 20), LoopBack = getAttr(integer, Attrs, loop_back, 0), MaxRestart = getAttr(integer, Attrs, max_restart, 3), SleepLoop = getAttr(integer, Attrs, sleep_loop, 5), ValRaw = getText(Element#xmlElement.content), RegExp = ts_utils:clean_str(ValRaw), SkipHeaders = getAttr(atom, Attrs, skip_headers, no), ApplyTo = case getAttr(string, Attrs, apply_to_content, undefined) of undefined -> undefined; Data -> {Mod, Fun} = ts_utils:split2(Data,$:), {list_to_atom(Mod), list_to_atom(Fun)} end, NewMatch = #match{regexp=RegExp,subst=Subst, do=Do,'when'=When, name=Name, sleep_loop=SleepLoop * 1000, skip_headers=SkipHeaders, loop_back=LoopBack, max_restart=MaxRestart, max_loop=MaxLoop, apply_to_content=ApplyTo}, lists:foldl(fun parse/2, Conf#config{ match=lists:append(Match, [NewMatch]) }, Element#xmlElement.content); %%% Parsing the option element parse(Element = #xmlElement{name=option, attributes=Attrs}, Conf = #config{session_tab = Tab}) -> case getAttr(atom, Attrs, type) of "" -> case getAttr(Attrs, name) of "thinktime" -> Val = getAttr(float_or_integer,Attrs, value), ets:insert(Tab,{{thinktime, value}, Val}), Random = case { getAttr(integer, Attrs, min), getAttr(integer, Attrs, max)} of {Min, Max } when is_integer(Min), is_integer(Max), Max > 0, Max >= Min -> {"range", Min, Max}; {"",""} -> getAttr(string,Attrs, random, ?config(thinktime_random)) end, ets:insert(Tab,{{thinktime, random}, Random}), Override = getAttr(string, Attrs, override, ?config(thinktime_override)), ets:insert(Tab,{{thinktime, override}, Override}), lists:foldl( fun parse/2, Conf, Element#xmlElement.content); "ssl_ciphers" -> Cipher = getAttr(string,Attrs, value, negotiate), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{ssl_ciphers=Cipher}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "ssl_versions" -> Protocol = getAttr(atom,Attrs, value, negotiate), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{ssl_versions=[Protocol]}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "ssl_reuse_sessions" -> case getAttr(atom,Attrs, value, true) of false -> application:set_env(tsung,ssl_session_cache, 0), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{reuse_sessions=false}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); true -> % default value, do nothing lists:foldl( fun parse/2, Conf, Element#xmlElement.content) end; "ssl_disable_sni" -> case getAttr(atom, Attrs, value, false) of true -> OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{disable_sni = true}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content ); false -> lists:foldl(fun parse/2, Conf, Element#xmlElement.content) end; "seed" -> Seed = getAttr(integer,Attrs, value, now), lists:foldl( fun parse/2, Conf#config{seed=Seed}, Element#xmlElement.content); "rate_limit" -> Rate = getAttr(integer,Attrs, value, 512), % 512KB/sec MaxBurst = getAttr(integer,Attrs, max, Rate), TokenBucket= #token_bucket{rate=1024*Rate div 1000, burst= 1024*MaxBurst}, lists:foldl( fun parse/2, Conf#config{rate_limit=TokenBucket}, Element#xmlElement.content); "hibernate" -> Hibernate = case getAttr(integer_or_string,Attrs, value, 10000 ) of "infinity" -> infinity; Seconds -> Seconds * 1000 end, lists:foldl( fun parse/2, Conf#config{hibernate=Hibernate}, Element#xmlElement.content); "ports_range" -> Min = getAttr(integer,Attrs, min, 1025), Max = getAttr(integer,Attrs, max, 65534), case getAttr(atom,Attrs, value, on) of off -> lists:foldl( fun parse/2, Conf,Element#xmlElement.content); on -> %%TODO: check if min > 1024 and max < 65536 lists:foldl( fun parse/2, Conf#config{ports_range={Min,Max}}, Element#xmlElement.content) end; "job_notify_port" -> Port = getAttr(integer,Attrs, value, ?config(job_notify_port)), lists:foldl( fun parse/2, Conf#config{job_notify_port=Port}, Element#xmlElement.content); "connect_timeout" -> ConnectTimeout = getAttr(integer,Attrs, value, ?config(connect_timeout)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{connect_timeout=ConnectTimeout}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "tcp_rcv_buffer" -> Size = getAttr(integer,Attrs, value, ?config(rcv_size)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{tcp_rcv_size=Size}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "udp_rcv_buffer" -> Size = getAttr(integer,Attrs, value, ?config(rcv_size)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{udp_rcv_size=Size}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "tcp_snd_buffer" -> Size = getAttr(integer,Attrs, value, ?config(snd_size)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{tcp_snd_size=Size}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "udp_snd_buffer" -> Size = getAttr(integer,Attrs, value, ?config(snd_size)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{udp_snd_size=Size}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "idle_timeout" -> Timeout = getAttr(integer,Attrs, value, ?config(idle_timeout)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{idle_timeout=Timeout}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "global_ack_timeout" -> Timeout = getAttr(integer,Attrs, value, ?config(global_ack_timeout)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{global_ack_timeout=Timeout}, ts_timer:set_timeout(Timeout), lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "max_retries" -> MaxRetries = getAttr(integer,Attrs, value, ?config(max_retries)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{max_retries=MaxRetries}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "retry_timeout" -> Timeout = getAttr(integer,Attrs, value, ?config(client_retry_timeout)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{retry_timeout=Timeout}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "websocket_path" -> Path = getAttr(string,Attrs, value, ?config(websocket_path)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{websocket_path=Path}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "websocket_frame" -> Frame = getAttr(string,Attrs, value, ?config(websocket_frame)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{websocket_frame=Frame}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "websocket_subprotocols" -> SubProtocols = getAttr(string,Attrs, value, ?config(websocket_subprotocols)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{websocket_subprotocols=SubProtocols}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "websocket_origin" -> Origin = getAttr(string,Attrs, value, ?config(websocket_origin)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{websocket_origin=Origin}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "bosh_path" -> Path = getAttr(string,Attrs, value, ?config(bosh_path)), OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{bosh_path=Path}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); "file_server" -> FileName = getAttr(Attrs, value), case file:read_file_info(FileName) of {ok, _} -> ok; {error, _Reason} -> exit({error, bad_filename, FileName}) end, Id = getAttr(atom, Attrs, id,default), lists:foldl( fun parse/2, Conf#config{file_server=[{Id, FileName} | Conf#config.file_server]}, Element#xmlElement.content); "local_file_server" -> FileName = getAttr(Attrs, value), case file:read_file_info(FileName) of {ok, _} -> ok; {error, _Reason} -> exit({error, bad_filename, FileName}) end, Id = getAttr(atom, Attrs, id,default), lists:foldl( fun parse/2, Conf#config{local_file_server=[{Id, FileName} | Conf#config.local_file_server]}, Element#xmlElement.content); "global_number" -> GlobalNumber = getAttr(integer, Attrs, value, ?config(global_number)), ts_timer:config(GlobalNumber), lists:foldl( fun parse/2, Conf, Element#xmlElement.content); "max_ssh_startup_per_core" -> MaxStartup = getAttr(integer,Attrs, value, 20), lists:foldl( fun parse/2, Conf#config{max_ssh_startup=MaxStartup}, Element#xmlElement.content); "ip_transparent" -> case getAttr(atom, Attrs, value, false) of true -> OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{ip_transparent = true}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); false -> lists:foldl( fun parse/2, Conf, Element#xmlElement.content) end; "ip_bind_address_no_port" -> case getAttr(atom, Attrs, value, false) of true -> OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{ip_bind_address_no_port = true}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); false -> lists:foldl( fun parse/2, Conf, Element#xmlElement.content) end; "tcp_reuseaddr" -> Reuseaddr = getAttr(atom, Attrs, value, false), case Reuseaddr of true -> OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{tcp_reuseaddr = Reuseaddr}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); false -> lists:foldl( fun parse/2, Conf, Element#xmlElement.content) end; "tcp_reuseport" -> Reuseport = getAttr(atom, Attrs, value, false), case Reuseport of true -> OldProto = Conf#config.proto_opts, NewProto = OldProto#proto_opts{tcp_reuseport = Reuseport}, lists:foldl( fun parse/2, Conf#config{proto_opts=NewProto}, Element#xmlElement.content); false -> lists:foldl( fun parse/2, Conf, Element#xmlElement.content) end; Other -> ?LOGF("Unknown option ~p !~n",[Other], ?WARN), lists:foldl( fun parse/2, Conf, Element#xmlElement.content) end; Module -> Module:parse_config(Element, Conf) end; %%% Parsing the thinktime element parse(Element = #xmlElement{name=thinktime, attributes=Attrs}, Conf = #config{curid=Id, session_tab = Tab, sessions = [CurS |_]}) -> {RT,T} = case getAttr(Attrs, value) of "wait_bidi" -> {infinity, infinity}; "wait_global" -> {wait_global,infinity}; "%%"++Tail -> % dynamic thinktime {"%%"++Tail,"%%"++Tail}; _ -> DefThink = get_default(Tab,{thinktime, value},thinktime_value), DefRandom = get_default(Tab,{thinktime, random},thinktime_random), {Think, Randomize} = case get_default(Tab,{thinktime, override},thinktime_override) of "true" -> {DefThink, DefRandom}; "false" -> case { getAttr(integer, Attrs, min), getAttr(integer, Attrs, max), getAttr(float_or_integer, Attrs, value)} of {Min, Max, "" } when is_integer(Min), is_integer(Max), Max > 0, Min >0, Max > Min -> {"", {"range", Min, Max} }; {"","",""} -> CurRandom = getAttr(string, Attrs,random,DefRandom), {DefThink, CurRandom}; {"","",CurThink} when CurThink > 0 -> CurRandom = getAttr(string, Attrs,random,DefRandom), {CurThink, CurRandom}; _ -> exit({error, bad_thinktime}) end end, Val=case Randomize of "true" -> {random, Think * 1000}; {"range", Min2, Max2} -> {range, Min2 * 1000, Max2 * 1000}; "false" -> round(Think * 1000) end, {Val, Think} end, ?LOGF("New thinktime ~p for id (~p:~p)~n",[RT, CurS#session.id, Id+1], ?INFO), ets:insert(Tab,{{CurS#session.id, Id+1}, {thinktime, RT}}), lists:foldl( fun parse/2, Conf#config{curthink=T,curid=Id+1}, Element#xmlElement.content); %% Parsing the setdynvars element parse(Element = #xmlElement{name=setdynvars, attributes=Attrs}, Conf = #config{session_tab = Tab, sessions=[CurS|_], curid=Id}) -> Vars = [ getAttr(atom,Attr,name,none) || #xmlElement{name=var,attributes=Attr} <- Element#xmlElement.content], Action = case getAttr(string,Attrs,sourcetype,"erlang") of "erlang" -> [Module,Callback] = string:tokens(getAttr(string,Attrs,callback,none),":"), {setdynvars,erlang,{list_to_atom(Module),list_to_atom(Callback)},Vars}; "eval" -> Snippet = getAttr(string,Attrs,code,""), Fun= ts_utils:eval(Snippet), true = is_function(Fun, 1), {setdynvars,code,Fun,Vars}; "file" -> Order = getAttr(atom,Attrs,order,iter), FileId = getAttr(atom,Attrs,fileid,none), case lists:keysearch(FileId,1,Conf#config.file_server) of {value,_Val} -> Delimiter = list_to_binary(getAttr(string,Attrs,delimiter,";")), {setdynvars,file,{Order,FileId,Delimiter},Vars}; false -> io:format(standard_error, "Unknown_file_id ~p in file setdynvars declaration: you forgot to add a file_server option~n",[FileId]), exit({error, unknown_file_id}) end; "local_file" -> FileId = getAttr(atom,Attrs,fileid,none), case lists:keysearch(FileId,1,Conf#config.local_file_server) of {value,_Val} -> Delimiter = list_to_binary(getAttr(string,Attrs,delimiter,";")), {setdynvars,local_file,{FileId,Delimiter},Vars}; false -> io:format(standard_error, "Unknown_file_id ~p in file setdynvars declaration: you forgot to add a local_file_server option~n",[FileId]), exit({error, unknown_file_id}) end; "random_string" -> Length = getAttr(integer,Attrs,length,20), {setdynvars,random,{string,Length},Vars}; "urandom_string" -> Length = getAttr(integer,Attrs,length,20), {setdynvars,urandom,{string,Length},Vars}; "random_number" -> Start = getAttr(integer,Attrs,start,1), End = getAttr(integer,Attrs,'end',10), {setdynvars,random,{number,Start,End},Vars}; "jsonpath" -> From = getAttr(atom, Attrs,from), JSONPath = getAttr(Attrs,jsonpath), {setdynvars,jsonpath,{JSONPath, From},Vars}; "value" -> Value = getAttr(string,Attrs,value,""), {setdynvars,value,{string,Value},Vars}; "server" -> {setdynvars,server,{},Vars} end, ?LOGF("Add setdynvars in session ~p as id ~p",[CurS#session.id,Id+1],?INFO), ets:insert(Tab, {{CurS#session.id, Id+1}, Action}), Conf#config{curid=Id+1}; %% Parsing other elements parse(Element = #xmlElement{}, Conf = #config{}) -> lists:foldl(fun parse/2, Conf, Element#xmlElement.content); %% Parsing non #xmlElement elements parse(_Element, Conf = #config{}) -> Conf. getAttr(Attr, Name) -> getAttr(string, Attr, Name, ""). %%%---------------------------------------------------------------------- %%% @spec getAttr(Type:: 'string'|'list'|'float_or_integer', Attr::list(), %%% Name::string()) -> term() %%% @doc search the attribute list for the given one %%% @end %%%---------------------------------------------------------------------- getAttr(Type, Attr, Name) -> getAttr(Type, Attr, Name, ""). getAttr(Type, [Attr = #xmlAttribute{name=Name}|_], Name, _Default) -> case { Attr#xmlAttribute.value, Type} of {[], string } -> "" ; {[], list } -> [] ; {[], float_or_integer } -> 0 ; {A,_} -> getTypeAttr(Type,A) end; getAttr(Type, [_H|T], Name, Default) -> getAttr(Type, T, Name, Default); getAttr(_Type, [], _Name, Default) -> Default. getTypeAttr(string, String)-> String; getTypeAttr(list, String)-> String; getTypeAttr(float_or_integer, String)-> case erl_scan:string(String) of {ok, [{integer,_,I}],_} -> I; {ok, [{float,_,F}],_} -> F end; getTypeAttr(integer_or_string, String)-> case erl_scan:string(String) of {ok, [{integer,_,I}],_} -> I; _ -> String end; getTypeAttr(Type, String) -> {ok, [{Type,_,Val}],_} = erl_scan:string(String), Val. %%%---------------------------------------------------------------------- %%% Function: getText/1 %%% Purpose: get the text of the XML node %%%---------------------------------------------------------------------- getText([#xmlText{value=Value}|_]) -> string:strip(Value, both); getText(_Other) -> "". %%%---------------------------------------------------------------------- %%% Function: to_seconds/2 %%% Purpose: get the real duration in seconds %%%---------------------------------------------------------------------- to_seconds("second", Val)-> Val; to_seconds("minute", Val)-> Val*60; to_seconds("hour", Val)-> Val*3600; to_seconds("millisecond", Val)-> Val/1000. to_milliseconds("second", Val)-> Val*1000; to_milliseconds("minute", Val)-> Val*60000; to_milliseconds("hour", Val)-> Val*3600000; to_milliseconds("millisecond", Val)-> Val. %%%---------------------------------------------------------------------- %%% Function: get_default/2 %%%---------------------------------------------------------------------- get_default(Tab, Key,ConfigName) when not is_tuple(Key) -> get_default(Tab, {Key, value},ConfigName); get_default(Tab, Key,ConfigName) -> case ets:lookup(Tab,Key) of [] -> ?config(ConfigName); [{_, SName}] -> SName end. get_default(Tab, Key) when is_atom(Key) -> get_default(Tab, Key, Key). %%%---------------------------------------------------------------------- %%% Function: mark_prev_req/3 %%% Purpose: use to set page marks in requests during parsing ; by %%% default, a new request is mark as an endpage; if a new request is %%% parse, then the previous one must be set to false, unless there is %%% a thinktime between them %%%---------------------------------------------------------------------- mark_prev_req(0, _, _) -> ok; mark_prev_req(Id, Tab, CurS) -> %% if the previous msg is a #ts_request request, set endpage to %% false, we are the current last request of the page case ets:lookup(Tab,{CurS#session.id, Id}) of [{Key, Msg=#ts_request{}}] -> ets:insert(Tab,{Key, Msg#ts_request{endpage=false}}); [{_, {transaction,_,_}}] ->% transaction, continue to search back mark_prev_req(Id-1, Tab, CurS); _ -> ok end. get_batch_nodes(pbs) -> get_batch_nodes(torque); get_batch_nodes(lsf)-> case os:getenv("LSB_HOSTS") of false -> []; Nodes -> lists:map(fun shortnames/1, string:tokens(Nodes, " ")) end; get_batch_nodes(oar) -> get_batch_nodes2("OAR_NODEFILE"); get_batch_nodes(torque) -> get_batch_nodes2("PBS_NODEFILE"). get_batch_nodes2(Env) -> case os:getenv(Env) of false -> []; NodeFile -> {ok, Nodes} = ts_utils:file_to_list(NodeFile), lists:map(fun shortnames/1, Nodes) end. shortnames(Hostname)-> [S | _]= string:tokens(Hostname,"."), S. %%---------------------------------------------------------------------- %% @spec: backup_config(Dir::string(), Name::string(), Config::tuple()) -> %% ok | {error, Reason::term()} %% @doc: create a backup copy of the config file in the log directory %% This is useful to have an history of all parameters of a test. %% Use parsed config file to expand all ENTITY %% @end %%---------------------------------------------------------------------- backup_config(Dir,standard_io, Config) -> backup_config(Dir, "tsung_stdin.xml", Config); backup_config(Dir, Name, Config) -> BaseName = filename:basename(Name), {ok,IOF}=file:open(filename:join(Dir,BaseName),[write]), Export=xmerl:export_simple([Config],xmerl_xml), case catch io:format(IOF,"~s~n",[lists:flatten(Export)]) of {'EXIT', _Error} -> % weird characters in the XML ? io:format(IOF,"~p~n",[lists:flatten(Export)]); _ -> ok end, file:close(IOF). %% @spec read_stdio()-> string() %% @doc Read config from standard input %% @end read_stdio()-> read_stdio(io:get_line(""),[]). read_stdio(eof, Data)-> lists:flatten(Data); read_stdio(Data,Acc) -> read_stdio(io:get_line(""),[Acc,Data]). set_net_type("tcp") -> ts_tcp; set_net_type("tcp6") -> ts_tcp6; set_net_type("udp") -> ts_udp; set_net_type("udp6") -> ts_udp6; set_net_type("ssl") -> ts_ssl; set_net_type("ssl6") -> ts_ssl6; set_net_type("websocket") -> ts_server_websocket; set_net_type("websocket_ssl") -> ts_server_websocket_ssl; set_net_type("ws") -> ts_server_websocket; set_net_type("wss") -> ts_server_websocket_ssl; set_net_type("bosh") -> ts_bosh; set_net_type("bosh_ssl") -> ts_bosh_ssl; set_net_type("erlang") -> ts_erlang. get_dynvar_name(VarNameStr) -> %% check if the var name is for an array (myvar[N]) case re:run(VarNameStr,"(.+)\[(\d+)\]",[{capture,all_but_first,list},dotall]) of {match,[Name,Index]} -> {list_to_atom(Name),Index}; _ -> list_to_atom(VarNameStr) end. %% @spec get_popularity(Proba::number(), Weight::number(), UseWeight::(true|false|undefined), Total::number()) -> %% {Value::number(), UseWeight::boolean(), Total::number()} %% @doc check if we are using popularity or weights; keep the total up to date. @end get_popularity(-1, -1, _, _)-> erlang:error({"must set weight or probability in session"}); get_popularity(Proba,Weight,_,_) when is_number(Proba), Proba >= 0, is_number(Weight), Weight >= 0 -> erlang:error({"can't mix probabilities and weights", Proba, Weight} ); get_popularity(Proba, _Weight, true,_) when is_number(Proba), Proba >= 0-> erlang:error({"can't use probability when using weight"}); get_popularity(_, Weight, false,_) when is_number(Weight), Weight >= 0-> erlang:error({"can't use weights when using probabilities"}); get_popularity(_, Weight, undefined,_) when is_number(Weight), Weight >= 0 -> {Weight, true, Weight}; get_popularity(Proba, _, undefined,Total) when is_number(Proba) -> {Proba, false, Proba+Total}; get_popularity(Proba, _, false,Total) when is_number(Proba) -> {Proba, false, Proba+Total}; get_popularity(_, Weight, true, Total) when is_number(Weight) -> {Weight, true, Weight+Total}. tsung-1.8.0/src/tsung_controller/ts_config_amqp.erl0000644000201100017670000002152014377756736022265 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_config_amqp). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -export([parse_config/2]). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_amqp.hrl"). -include("xmerl.hrl"). %%---------------------------------------------------------------------- %% Func: parse_config/2 %% Args: Element, Config %% Returns: List %% Purpose: parse a request defined in the XML config file %%---------------------------------------------------------------------- %% Parsing other elements parse_config(Element = #xmlElement{name = dyn_variable}, Conf = #config{}) -> ts_config:parse(Element, Conf); parse_config(Element = #xmlElement{name = amqp}, Config = #config{curid = Id, session_tab = Tab, sessions = [CurS | _], dynvar = DynVar, subst = SubstFlag, match = MatchRegExp}) -> initialize_options(Tab), TypeStr = ts_config:getAttr(string, Element#xmlElement.attributes, type), Type = list_to_atom(TypeStr), ReqList = case Type of %% connection.open request, we add all the requests to be done 'connection.open' -> ['connect', 'connection.start_ok', 'connection.tune_ok', 'connection.open']; 'waitForConfirms' -> Timeout = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, timeout, 1), ets:insert(Tab, {{CurS#session.id, Id}, {thinktime, Timeout * 1000}}), []; 'waitForMessages' -> Timeout = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, timeout, 60), ets:insert(Tab, {{CurS#session.id, Id}, {thinktime, Timeout * 1000}}), []; _ -> [Type] end, Result = lists:foldl(fun(RequestType, CurrId) -> {Ack, Request} = parse_request(Element, RequestType, Tab), Msg = #ts_request{ack = Ack, endpage = true, dynvar_specs = DynVar, subst = SubstFlag, match = MatchRegExp, param = Request}, ets:insert(Tab, {{CurS#session.id, CurrId}, Msg}), CurrId + 1 end, Id, ReqList), ResultId = case ReqList of [] -> Id; _ -> Result - 1 end, ts_config:mark_prev_req(Id - 1, Tab, CurS), lists:foldl(fun(A, B) -> ts_config:parse(A, B) end, Config#config{dynvar = [], curid = ResultId}, Element#xmlElement.content); %% Parsing options parse_config(Element = #xmlElement{name=option}, Conf = #config{session_tab = Tab}) -> NewConf = case ts_config:getAttr(Element#xmlElement.attributes, name) of "username" -> Val = ts_config:getAttr(string,Element#xmlElement.attributes, value,?AMQP_USER), ets:insert(Tab,{{amqp_username,value}, Val}), Conf; "password" -> Val = ts_config:getAttr(string,Element#xmlElement.attributes, value,?AMQP_PASSWORD), ets:insert(Tab,{{amqp_password,value}, Val}), Conf; "heartbeat" -> Val = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, value, 600), ets:insert(Tab,{{amqp_heartbeat,value}, Val}), Conf end, lists:foldl(fun(A,B) -> ts_config:parse(A,B) end, NewConf, Element#xmlElement.content); %% Parsing other elements parse_config(Element = #xmlElement{}, Conf = #config{}) -> ts_config:parse(Element,Conf); %% Parsing non #xmlElement elements parse_config(_, Conf = #config{}) -> Conf. parse_request(Element, Type = 'connection.open', _Tab) -> Vhost = ts_config:getAttr(string, Element#xmlElement.attributes, vhost, "/"), Request = #amqp_request{type = Type, vhost = Vhost}, {parse, Request}; parse_request(_Element, Type = 'connection.start_ok', Tab) -> UserName = ts_config:get_default(Tab, amqp_username), Password = ts_config:get_default(Tab, amqp_password), Request = #amqp_request{type = Type, username = UserName, password = Password}, {parse, Request}; parse_request(_Element, Type = 'connection.tune_ok', Tab) -> HeartBeat = ts_config:get_default(Tab, amqp_heartbeat), Request = #amqp_request{type = Type, heartbeat = HeartBeat}, {no_ack, Request}; parse_request(Element, Type = 'channel.open', _Tab) -> Channel = ts_config:getAttr(string, Element#xmlElement.attributes, channel, "0"), Request = #amqp_request{type = Type, channel = Channel}, {parse, Request}; parse_request(Element, Type = 'basic.publish', _Tab) -> Channel = ts_config:getAttr(string, Element#xmlElement.attributes, channel, "1"), Exchange = ts_config:getAttr(string, Element#xmlElement.attributes, exchange, ""), RoutingKey = ts_config:getAttr(string, Element#xmlElement.attributes, routing_key, "/"), Size = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, payload_size, 100), PersistentStr = ts_config:getAttr(string, Element#xmlElement.attributes, persistent, "false"), Payload = ts_config:getAttr(string, Element#xmlElement.attributes, payload, ""), Persistent = list_to_atom(PersistentStr), Request = #amqp_request{type = Type, channel = Channel, exchange = Exchange, routing_key = RoutingKey, payload_size = Size, payload = Payload, persistent = Persistent}, {no_ack, Request}; parse_request(Element, Type = 'basic.consume', _Tab) -> Channel = ts_config:getAttr(string, Element#xmlElement.attributes, channel, "1"), Queue = ts_config:getAttr(string, Element#xmlElement.attributes, queue, ""), AckStr = ts_config:getAttr(string, Element#xmlElement.attributes, ack, "false"), Ack = list_to_atom(AckStr), Request = #amqp_request{type = Type, channel = Channel, queue = Queue, ack = Ack}, {parse, Request}; parse_request(Element, Type = 'basic.qos', _Tab) -> Channel = ts_config:getAttr(string, Element#xmlElement.attributes, channel, "1"), PrefetchSize = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, prefetch_size, 0), PrefetchCount = ts_config:getAttr(float_or_integer, Element#xmlElement.attributes, prefetch_count, 0), Request = #amqp_request{type = Type, channel = Channel, prefetch_size = PrefetchSize, prefetch_count = PrefetchCount}, {parse, Request}; parse_request(Element, Type, _Tab) -> Channel = ts_config:getAttr(string, Element#xmlElement.attributes, channel, "1"), Request = #amqp_request{type = Type, channel = Channel}, {parse, Request}. initialize_options(Tab) -> case ts_config:get_default(Tab, amqp_initialized) of {undef_var, _} -> ets:insert_new(Tab,{{amqp_username,value}, ?AMQP_USER}), ets:insert_new(Tab,{{amqp_password,value}, ?AMQP_PASSWORD}), ets:insert_new(Tab,{{amqp_heartbeat,value}, 600}); _Else -> ok end. tsung-1.8.0/src/tsung_controller/ts_api.erl0000644000201100017670000000412314377756736020553 0ustar nniclausdream%%% %%% Copyright 2014 Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 23 avril 2014 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% -module(ts_api). -vc('$Id: ts_web.erl,v 0.0 2014/04/23 12:12:17 nniclaus Exp $ '). -author('nicolas@niclux.org'). -include("ts_macros.hrl"). -include_lib("kernel/include/file.hrl"). -export([status/3]). status(SessionID, _Env, _Input) -> {ok, Nodes, Ended_Beams, MaxPhases} = ts_config_server:status(), Active = Nodes - Ended_Beams, {Clients, ReqRate, Connected, Interval, Phase, Cpu} = ts_mon:status(), NPhase = case Phase of error -> 1; {ok,N} -> (N div Nodes) + 1 end, JSON = "{\"phase\": "++ integer_to_list(NPhase) ++ "," ++ "\"phase_total\": "++ integer_to_list(MaxPhases) ++ "," ++ "\"users\": "++ integer_to_list(Clients) ++ "," ++ "\"connected_users\": "++ integer_to_list(Connected) ++ "," ++ "\"request_rate\": "++ ts_web:number_to_list(ReqRate/Interval) ++ "," ++ "\"active_beams\": "++ integer_to_list(Active) ++ "," ++ "\"cpu_controller\": "++ ts_web:number_to_list(Cpu) ++ "}", mod_esi:deliver(SessionID, [ "Content-Type: application/json\r\n\r\n", JSON ]). tsung-1.8.0/src/lib/0000755000201100017670000000000014377757020013716 5ustar nniclausdreamtsung-1.8.0/src/lib/websocket.erl0000644000201100017670000002050614377756736016430 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(websocket). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -export([get_handshake/6, check_handshake/2, encode_binary/1, encode_text/1, encode_close/1, encode/2, decode/1]). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_websocket.hrl"). %%%=================================================================== %%% API functions %%%=================================================================== get_handshake(Host, Path, SubProtocol, Version, Origin, Headers) -> {Key, Accept} = gen_accept_key(), Req = list_to_binary(["GET ", Path, " HTTP/1.1\r\n", "Host: ", Host ,"\r\n", "Upgrade: websocket\r\n", "Connection: Upgrade\r\n", "Origin: ", case Origin of "" -> Host; _ -> Origin end, "\r\n", "Sec-WebSocket-Key: ", Key, "\r\n", "Sec-WebSocket-Version: ", Version, "\r\n", headers(Headers)]), SubProHeader = case SubProtocol of [] -> []; _ -> "Sec-WebSocket-Protocol: " ++ SubProtocol ++ "\r\n" end, Handshake = list_to_binary([Req, SubProHeader, "\r\n" ]), {Handshake, Accept}. check_handshake(Response, Accept) -> ?DebugF("check handshake, response is : ~p~n",[Response]), [HeaderPart, _] = binary:split(Response, <<"\r\n\r\n">>), [StatusLine | Headers] = binary:split(HeaderPart, <<"\r\n">>, [global, trim]), Map = dict:new(), {Prefix, _} = split_binary(StatusLine, 12), [Version, Code] = binary:split(Prefix, <<" ">>), Map1 = dict:store("version", string:to_lower(binary_to_list(Version)), Map), Map2 = dict:store("status", binary_to_list(Code), Map1), MapFun = fun(HeaderLine, Acc) -> [Header, Value] = binary:split(HeaderLine, <<": ">>), HeaderStr = string:to_lower(binary_to_list(Header)), ValueStr = case HeaderStr of "sec-websocket-accept" -> binary_to_list(Value); _ -> string:to_lower(binary_to_list(Value)) end, dict:store(HeaderStr, ValueStr, Acc) end, HeaderMap = lists:foldl(MapFun, Map2, Headers), RequiredHeaders = [{"Version", "HTTP/1.1"}, {"Status", "101"}, {"Upgrade", "websocket"}, {"Connection", "Upgrade"}, {"Sec-WebSocket-Accept", Accept}], lists:foldl(fun(_, Acc = {error, _}) -> Acc; ({Key, Value}, ok) -> TargetKey = string:to_lower(Key), TargetValue = case TargetKey of "sec-websocket-accept" -> Value; _ -> string:to_lower(Value) end, case dict:is_key(TargetKey, HeaderMap) of true -> case dict:find(TargetKey, HeaderMap) of {ok, TargetValue} -> ok; {ok, Other} -> {error, {mismatch, Key, Value, Other}} end; _ -> {error, {miss_headers, Key}} end end, ok, RequiredHeaders). encode_binary(Data) -> encode(Data, ?OP_BIN). encode_text(Data) -> encode(Data, ?OP_TEXT). encode_close(Reason) -> %% According RFC6455, we should add a status code for close frame, %% check here: http://tools.ietf.org/html/rfc6455#section-7.4, %% we add a normal closure status code 1000 here. StatusCode = <<3, 232>>, Data = <>, encode(Data, ?OP_CLOSE). encode(Data, Opcode) -> Key = crypto:strong_rand_bytes(4), PayloadLen = erlang:size(Data), MaskedData = mask(Data, Key), Length = if PayloadLen < 126 -> <>; PayloadLen < 65536 -> <<126:7, PayloadLen:16>>; true -> <<127:7, PayloadLen:64>> end, <<1:1, 0:3, Opcode:4, 1:1, Length/bitstring, Key/binary, MaskedData/bitstring>>. decode(Data) -> parse_frame(Data). %%%=================================================================== %%% Internal functions %%%=================================================================== gen_accept_key() -> random:seed(erlang:now()), Key = crypto:strong_rand_bytes(16), KeyStr = base64:encode_to_string(Key), Accept = binary:list_to_bin(KeyStr ++ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"), AcceptStr = base64:encode_to_string(crypto:hash(sha, Accept)), {KeyStr, AcceptStr}. %%%=================================================================== %% NOTE: The code of the following two functions are borrowed from %% https://github.com/wulczer/tsung_ws/blob/master/src/tsung/ts_websocket.erl. % mask(binary, binary) -> binary % % Mask the given payload using a 4 byte masking key. mask(Payload, MaskingKey) -> % create a mask with the same length as the payload by repeating % the masking key Div = size(Payload) div size(MaskingKey), Rem = size(Payload) rem size(MaskingKey), LongPart = binary:copy(MaskingKey, Div), Rest = binary:part(MaskingKey, {0, Rem}), Mask = << LongPart/bitstring, Rest/bitstring >>, % xor the payload and the mask crypto:exor(Payload, Mask). % parse_payload(integer, binary) -> {integer, binary, binary} | more % % Try to parse out a frame payload from binary data. Gets passed an % opcode and returns a tuple of opcode, payload and remaining data. If % not enough data is available, return a more atom. parse_payload(Opcode, << 0:1, % MASK Length:7, Payload:Length/binary, Rest/bitstring >>) when Length < 126 -> {Opcode, Payload, Rest}; parse_payload(Opcode, << 0:1, % MASK 126:7, Length:16, Payload:Length/binary, Rest/bitstring >>) when Length < 65536 -> {Opcode, Payload, Rest}; parse_payload(Opcode, << 0:1, % MASK 127:7, 0:1, Length:63, Payload:Length/binary, Rest/bitstring >>) -> {Opcode, Payload, Rest}; parse_payload(_Opcode, _Data) -> more. % parse_frame(binary) -> {integer, binary, binary} | more % % Try to parse out a WebSocket frame from binary data. Returns a tuple % of opcode, payload and remaining data or a more atom if not enough % data is available. parse_frame(<< 1:1, % FIN 0:3, % RSV Opcode:4, % OPCODE MaskLengthAndPayload/bitstring >>) -> parse_payload(Opcode, MaskLengthAndPayload); parse_frame(_Data) -> more. % user defined headers headers([]) -> []; headers(Headers) -> HeadersToIgnore = ["host", "upgrade", "connection", "origin", "sec-websocket-key", "sec-websocket-version", "sec-websocket-protocol"], lists:foldl(fun({Name, Value}, Result) -> case lists:member(string:to_lower(Name), HeadersToIgnore) of true -> Result; _ -> [Name, ": ", Value, ?CRLF | Result] end end, [], lists:reverse(Headers)). tsung-1.8.0/src/lib/uuid.erl0000644000201100017670000001327714377756736015417 0ustar nniclausdream%% @author Andrew Kreiling %% @copyright 2008 Andrew Kreiling . All Rights Reserved. %% %% @copyright 2010 Nicolas Niclausse: Add random_str fun %% %% @license : MIT %% %% @doc %% UUID module for Erlang %% %% This Erlang module was designed to be a simple library for generating UUIDs. It %% conforms to RFC 4122 whenever possible. %% -module(uuid). -author('Andrew Kreiling '). -behaviour(gen_server). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. -export([start/0, start/1, start_link/0, start_link/1, stop/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([v4/0, random/0, srandom/0, sha/2, md5/2, timestamp/0, timestamp/2, to_string/1, random_str/0]). -define(SERVER, ?MODULE). -define(UUID_DNS_NAMESPACE, <<107,167,184,16,157,173,17,209,128,180,0,192,79,212,48,200>>). -define(UUID_URL_NAMESPACE, <<107,167,184,17,157,173,17,209,128,180,0,192,79,212,48,200>>). -define(UUID_OID_NAMESPACE, <<107,167,184,18,157,173,17,209,128,180,0,192,79,212,48,200>>). -define(UUID_X500_NAMESPACE, <<107,167,184,20,157,173,17,209,128,180,0,192,79,212,48,200>>). -record(state, {node, clock_seq}). %% @type uuid() = binary(). A binary representation of a UUID %% @spec v4() -> uuid() %% @equiv random() %% @deprecated Please use the function random() instead. %% v4() -> random(). %% @spec random_str() -> string() %% @doc %% Generates a string representation of a random UUID %% random_str() -> to_string(random()). %% @spec random() -> uuid() %% @doc %% Generates a random UUID %% random() -> U = << (random:uniform(4294967296) - 1):32, (random:uniform(4294967296) - 1):32, (random:uniform(4294967296) - 1):32, (random:uniform(4294967296) - 1):32 >>, format_uuid(U, 4). %% @spec srandom() -> uuid() %% @doc %% Seeds random number generation with erlang:now() and generates a random UUID %% srandom() -> {A1,A2,A3} = erlang:now(), random:seed(A1, A2, A3), random(). %% @spec sha(Namespace, Name) -> uuid() %% where %% Namespace = dns | url | oid | x500 | uuid() %% Name = list() | binary() %% @doc %% Generates a UUID based on a crypto:hash(sha) hash %% sha(Namespace, Name) when is_list(Name) -> sha(Namespace, list_to_binary(Name)); sha(Namespace, Name) -> Context = crypto:hash_update(crypto:hash_update(crypto:hash_init(sha), namespace(Namespace)), Name), U = crypto:hash_final(Context), format_uuid(U, 5). %% @spec md5(Namespace, Name) -> uuid() %% where %% Namespace = dns | url | oid | x500 | uuid() %% Name = list() | binary() %% @doc %% Generates a UUID based on a crypto:hash(md5) hash %% md5(Namespace, Name) when is_list(Name) -> md5(Namespace, list_to_binary(Name)); md5(Namespace, Name) -> Context = crypto:hash_update(crypto:hash_update(crypto:hash_init(md5), namespace(Namespace)), Name), U = crypto:hash_final(Context), format_uuid(U, 3). %% @spec timestamp() -> uuid() %% @doc %% Generates a UUID based on timestamp %% %% Requires that the uuid gen_server is started %% timestamp() -> gen_server:call(?SERVER, timestamp). %% @spec timestamp(Node, CS) -> uuid() %% where %% Node = binary() %% CS = int() %% @doc %% Generates a UUID based on timestamp %% timestamp(Node, CS) -> {MegaSecs, Secs, MicroSecs} = erlang:now(), T = (((((MegaSecs * 1000000) + Secs) * 1000000) + MicroSecs) * 10) + 16#01b21dd213814000, format_uuid(T band 16#ffffffff, (T bsr 32) band 16#ffff, (T bsr 48) band 16#ffff, (CS bsr 8) band 16#ff, CS band 16#ff, Node, 1). %% @spec to_string(UUID) -> string() %% where %% UUID = uuid() %% @doc %% Generates a string representation of a UUID %% to_string(<> = _UUID) -> lists:flatten(io_lib:format("~8.16.0b-~4.16.0b-~4.16.0b-~2.16.0b~2.16.0b-~12.16.0b", [TL, TM, THV, CSR, CSL, N])). %% %% uuid gen_server for generating timestamps with saved state %% start() -> start([]). start(Args) -> gen_server:start({local, ?SERVER}, ?MODULE, Args, []). start_link() -> start_link([]). start_link(Args) -> gen_server:start_link({local, ?SERVER}, ?MODULE, Args, []). stop() -> gen_server:cast(?SERVER, stop). init(Options) -> {A1,A2,A3} = proplists:get_value(seed, Options, erlang:now()), random:seed(A1, A2, A3), State = #state{ node = proplists:get_value(node, Options, <<0:48>>), clock_seq = random:uniform(65536) }, error_logger:info_report("uuid server started"), {ok, State}. handle_call(timestamp, _From, State) -> Reply = timestamp(State#state.node, State#state.clock_seq), {reply, Reply, State}; handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. handle_cast(stop, State) -> {stop, normal, State}; handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> error_logger:info_report("uuid server stopped"), ok. code_change(_OldVsn, State, _Extra) -> {ok, State}. %% %% Internal API %% namespace(dns) -> ?UUID_DNS_NAMESPACE; namespace(url) -> ?UUID_URL_NAMESPACE; namespace(oid) -> ?UUID_OID_NAMESPACE; namespace(x500) -> ?UUID_X500_NAMESPACE; namespace(UUID) when is_binary(UUID) -> UUID; namespace(_) -> error. format_uuid(TL, TM, THV, CSR, CSL, <>, V) -> format_uuid(<>, V); format_uuid(TL, TM, THV, CSR, CSL, N, V) -> format_uuid(<>, V). format_uuid(<>, V) -> <>. %% vim:sw=4:sts=4:ts=8:et tsung-1.8.0/src/lib/rfc4515_parser.erl0000644000201100017670000001207114377756736017105 0ustar nniclausdream%% Parsing functions for RFC4515 (String Representation of LDAP Search Filters) %% %% TODO: extensibleMatch features not implemented. %% %% Author : Pablo Polvorin -module(rfc4515_parser). -author('ppolv@yahoo.com.ar'). -export([tokenize/1,filter/1,filter_to_string/1]). %% tokenize/1 %% Tokenize the input string into a token list. Generated tokens: %% '(' , ')' , '=' , '<=' ,'>=' , '~=' , '*' , '&' , '|' , '*' , {text,Value} %% Ej: %% ldap_parser:tokenize("(&(!(prpr=a*s*sse*y))(pr~=sss))"). %% --> ['(', '&','(', '!', '(', {text,"prpr"}, '=', {text,"a"}, '*', {text,"s"}, '*', %% {text,"sse"},'*', {text,"y"}, ')', ')', '(', {text,"pr"}, '~=', {text,"sss"}, ')', ')'] %% tokenize(L) -> tokenizer(lists:flatten(L),[],[]). %%flatten because & ,etc. in xml attributes ends in deep lists ej:[[38]] tokenizer([$(|L],Current,Tokens) -> tokenizer(L,[],['('|add_current(Current,Tokens)]); tokenizer([$)|L],Current,Tokens) -> tokenizer(L,[],[')'|add_current(Current,Tokens)]); tokenizer([$&|L],Current,Tokens) -> tokenizer(L,[],['&'|add_current(Current,Tokens)]); tokenizer([$||L],Current,Tokens) -> tokenizer(L,[],['|'|add_current(Current,Tokens)]); tokenizer([$!|L],Current,Tokens) -> tokenizer(L,[],['!'|add_current(Current,Tokens)]); tokenizer([$=|L],Current,Tokens) -> tokenizer(L,[],['='|add_current(Current,Tokens)]); tokenizer([$*|L],Current,Tokens) -> tokenizer(L,[],['*'|add_current(Current,Tokens)]); tokenizer([$>,$=|L],Current,Tokens) -> tokenizer(L,[],['>='|add_current(Current,Tokens)]); tokenizer([$<,$=|L],Current,Tokens) -> tokenizer(L,[],['<='|add_current(Current,Tokens)]); tokenizer([$~,$=|L],Current,Tokens) -> tokenizer(L,[],['~='|add_current(Current,Tokens)]); %% an encoded valued start with a backslash '\' character followed by the %%two hexadecimal digits representing the ASCII value of the encoded character tokenizer([92|L],Current,Tokens) -> [H1,H2|L2] = L, tokenizer(L2,[decode([H1,H2])|Current],Tokens); tokenizer([C|L],Current,Tokens) -> tokenizer(L,[C|Current],Tokens); tokenizer([],[],Tokens) -> lists:reverse(Tokens). %%FIXME: accept trailing whitespaces add_current(Current,Tokens) -> case string:strip(Current) of [] -> Tokens ; X -> [{text,lists:reverse(X)}|Tokens] end. decode(Hex) -> {ok,[C],[]} = io_lib:fread("~#","16#" ++ Hex), C. %% filter/1 %% parse a token list into an AST-like structure. %% Ej: %% ldap_parser:filter(ldap_parser:tokenize("(&(!(prpr=a*s*sse*y))(pr~=sss))")). %% --> {{'and',[{'not',{substring,"prpr", %% [{initial,"a"}, %% {any,"s"}, %% {any,"sse"}, %% {final,"y"}]}}, %% {aprox,"pr","sss"}]}, %% []} filter(['('|L]) -> {R, [')'|L2]} =filtercomp(L), {R,L2}. filtercomp(['&'|L]) -> {R,L2} = filterlist(L), {{'and',R},L2}; filtercomp(['|'|L]) -> {R,L2} = filterlist(L), {{'or',R},L2}; filtercomp(['!'|L]) -> {R,L2} = filter(L), {{'not',R},L2}; filtercomp(L) -> item(L). filterlist(L) -> filterlist(L,[]). filterlist(L=[')'|_],List) -> {lists:reverse(List),L}; %% ')' marks the end of the filter list filterlist(L,List) -> {R,L2} = filter(L), filterlist(L2,[R|List]). item([{text,T}|L]) -> item2(L,T). item2(['~=',{text,V}|L],Attr) -> {{aprox,Attr,V},L}; item2(['>=',{text,V}|L],Attr) -> {{get,Attr,V},L}; item2(['<=',{text,V}|L],Attr) -> {{'let',Attr,V},L}; item2(['='|L],Attr) -> item3(L,Attr). % could be a presence, equality or substring match item3(L = [')'|_],Attr) -> {{eq,Attr,""},L}; %empty attr ej: (description=) item3(['*',')'|L],Attr) -> {{present,Attr},[')'|L]}; %presence ej: (description=*) item3([{text,V},')'|L],Attr) -> {{eq,Attr,V},[')'|L]}; %eq ej : = (description=some description) item3(L,Attr) -> {R,L2} = substring(L), {{substring,Attr,R},L2}. substring([{text,V},'*'|L]) -> any(L,[{initial,V}]); substring(['*'|L]) -> any(L,[]). any([{text,V},'*'|L],Subs) -> any(L,[{'any',V}|Subs]); any([{text,V},')'|L],Subs) -> {lists:reverse([{final,V}|Subs]),[')'|L]}; any(L = [')'|_],Subs) -> {lists:reverse(Subs),L}. filter_to_string({'and',L}) -> io_lib:format("(& ~s)",[lists:map(fun filter_to_string/1,L)]); filter_to_string({'or',L}) -> io_lib:format("(| ~s)",[lists:map(fun filter_to_string/1,L)]); filter_to_string({'not',I}) -> io_lib:format("(! ~s)",[filter_to_string(I)]); filter_to_string({'present',Attr}) -> io_lib:format("(~s=*)",[Attr]); filter_to_string({'substring',Attr,Subs}) -> io_lib:format("(~s=~s)",[Attr,print_substrings(Subs)]); filter_to_string({'aprox',Attr,Value}) -> io_lib:format("(~s~~=~s)",[Attr,Value]); filter_to_string({'let',Attr,Value}) -> io_lib:format("(~s<=~s)",[Attr,Value]); filter_to_string({'get',Attr,Value}) -> io_lib:format("(~s>=~s)",[Attr,Value]); filter_to_string({'eq',Attr,Value}) -> io_lib:format("(~s=~s)",[Attr,Value]). print_substrings(Subs) -> lists:map(fun print_substring/1,Subs). print_substring({initial,V}) -> io_lib:format("~s",[V]); print_substring({final,V}) -> io_lib:format("*~s",[V]); print_substring({any,V}) -> io_lib:format("*~s",[V]). tsung-1.8.0/src/lib/rabbit_misc.erl0000644000201100017670000000167414377756736016725 0ustar nniclausdream-module(rabbit_misc). -include("rabbit.hrl"). -include("rabbit_framing.hrl"). -export([method_record_type/1]). -export([amqp_error/4]). -export([frame_error/2]). -export([protocol_error/3, protocol_error/4, protocol_error/1]). method_record_type(Record) -> element(1, Record). amqp_error(Name, ExplanationFormat, Params, Method) -> Explanation = format(ExplanationFormat, Params), #amqp_error{name = Name, explanation = Explanation, method = Method}. format(Fmt, Args) -> lists:flatten(io_lib:format(Fmt, Args)). frame_error(MethodName, BinaryFields) -> protocol_error(frame_error, "cannot decode ~w", [BinaryFields], MethodName). protocol_error(Name, ExplanationFormat, Params) -> protocol_error(Name, ExplanationFormat, Params, none). protocol_error(Name, ExplanationFormat, Params, Method) -> protocol_error(amqp_error(Name, ExplanationFormat, Params, Method)). protocol_error(#amqp_error{} = Error) -> exit(Error). tsung-1.8.0/src/lib/rabbit_framing_amqp_0_9_1.erl0000644000201100017670000017434714377756736021332 0ustar nniclausdream%% Autogenerated code. Do not edit. %% %% The contents of this file are subject to the Mozilla Public License %% Version 1.1 (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.mozilla.org/MPL/ %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and %% limitations under the License. %% %% The Original Code is RabbitMQ. %% %% The Initial Developer of the Original Code is VMware, Inc. %% Copyright (c) 2007-2013 VMware, Inc. All rights reserved. %% -module(rabbit_framing_amqp_0_9_1). -include("rabbit_framing.hrl"). -export([version/0]). -export([lookup_method_name/1]). -export([lookup_class_name/1]). -export([method_id/1]). -export([method_has_content/1]). -export([is_method_synchronous/1]). -export([method_record/1]). -export([method_fieldnames/1]). -export([decode_method_fields/2]). -export([decode_properties/2]). -export([encode_method_fields/1]). -export([encode_properties/1]). -export([lookup_amqp_exception/1]). -export([amqp_exception/1]). %% Various types -ifdef(use_specs). -export_type([amqp_field_type/0, amqp_property_type/0, amqp_table/0, amqp_array/0, amqp_value/0, amqp_method_name/0, amqp_method/0, amqp_method_record/0, amqp_method_field_name/0, amqp_property_record/0, amqp_exception/0, amqp_exception_code/0, amqp_class_id/0]). -type(amqp_field_type() :: 'longstr' | 'signedint' | 'decimal' | 'timestamp' | 'table' | 'byte' | 'double' | 'float' | 'long' | 'short' | 'bool' | 'binary' | 'void' | 'array'). -type(amqp_property_type() :: 'shortstr' | 'longstr' | 'octet' | 'short' | 'long' | 'longlong' | 'timestamp' | 'bit' | 'table'). -type(amqp_table() :: [{binary(), amqp_field_type(), amqp_value()}]). -type(amqp_array() :: [{amqp_field_type(), amqp_value()}]). -type(amqp_value() :: binary() | % longstr integer() | % signedint {non_neg_integer(), non_neg_integer()} | % decimal amqp_table() | amqp_array() | byte() | % byte float() | % double integer() | % long integer() | % short boolean() | % bool binary() | % binary 'undefined' | % void non_neg_integer() % timestamp ). -type(amqp_method_name() :: ( 'connection.start' | 'connection.start_ok' | 'connection.secure' | 'connection.secure_ok' | 'connection.tune' | 'connection.tune_ok' | 'connection.open' | 'connection.open_ok' | 'connection.close' | 'connection.close_ok' | 'channel.open' | 'channel.open_ok' | 'channel.flow' | 'channel.flow_ok' | 'channel.close' | 'channel.close_ok' | 'access.request' | 'access.request_ok' | 'exchange.declare' | 'exchange.declare_ok' | 'exchange.delete' | 'exchange.delete_ok' | 'exchange.bind' | 'exchange.bind_ok' | 'exchange.unbind' | 'exchange.unbind_ok' | 'queue.declare' | 'queue.declare_ok' | 'queue.bind' | 'queue.bind_ok' | 'queue.purge' | 'queue.purge_ok' | 'queue.delete' | 'queue.delete_ok' | 'queue.unbind' | 'queue.unbind_ok' | 'basic.qos' | 'basic.qos_ok' | 'basic.consume' | 'basic.consume_ok' | 'basic.cancel' | 'basic.cancel_ok' | 'basic.publish' | 'basic.return' | 'basic.deliver' | 'basic.get' | 'basic.get_ok' | 'basic.get_empty' | 'basic.ack' | 'basic.reject' | 'basic.recover_async' | 'basic.recover' | 'basic.recover_ok' | 'basic.nack' | 'tx.select' | 'tx.select_ok' | 'tx.commit' | 'tx.commit_ok' | 'tx.rollback' | 'tx.rollback_ok' | 'confirm.select' | 'confirm.select_ok' )). -type(amqp_method() :: ( {10, 10} | {10, 11} | {10, 20} | {10, 21} | {10, 30} | {10, 31} | {10, 40} | {10, 41} | {10, 50} | {10, 51} | {20, 10} | {20, 11} | {20, 20} | {20, 21} | {20, 40} | {20, 41} | {30, 10} | {30, 11} | {40, 10} | {40, 11} | {40, 20} | {40, 21} | {40, 30} | {40, 31} | {40, 40} | {40, 51} | {50, 10} | {50, 11} | {50, 20} | {50, 21} | {50, 30} | {50, 31} | {50, 40} | {50, 41} | {50, 50} | {50, 51} | {60, 10} | {60, 11} | {60, 20} | {60, 21} | {60, 30} | {60, 31} | {60, 40} | {60, 50} | {60, 60} | {60, 70} | {60, 71} | {60, 72} | {60, 80} | {60, 90} | {60, 100} | {60, 110} | {60, 111} | {60, 120} | {90, 10} | {90, 11} | {90, 20} | {90, 21} | {90, 30} | {90, 31} | {85, 10} | {85, 11} )). -type(amqp_method_record() :: ( #'connection.start'{} | #'connection.start_ok'{} | #'connection.secure'{} | #'connection.secure_ok'{} | #'connection.tune'{} | #'connection.tune_ok'{} | #'connection.open'{} | #'connection.open_ok'{} | #'connection.close'{} | #'connection.close_ok'{} | #'channel.open'{} | #'channel.open_ok'{} | #'channel.flow'{} | #'channel.flow_ok'{} | #'channel.close'{} | #'channel.close_ok'{} | #'access.request'{} | #'access.request_ok'{} | #'exchange.declare'{} | #'exchange.declare_ok'{} | #'exchange.delete'{} | #'exchange.delete_ok'{} | #'exchange.bind'{} | #'exchange.bind_ok'{} | #'exchange.unbind'{} | #'exchange.unbind_ok'{} | #'queue.declare'{} | #'queue.declare_ok'{} | #'queue.bind'{} | #'queue.bind_ok'{} | #'queue.purge'{} | #'queue.purge_ok'{} | #'queue.delete'{} | #'queue.delete_ok'{} | #'queue.unbind'{} | #'queue.unbind_ok'{} | #'basic.qos'{} | #'basic.qos_ok'{} | #'basic.consume'{} | #'basic.consume_ok'{} | #'basic.cancel'{} | #'basic.cancel_ok'{} | #'basic.publish'{} | #'basic.return'{} | #'basic.deliver'{} | #'basic.get'{} | #'basic.get_ok'{} | #'basic.get_empty'{} | #'basic.ack'{} | #'basic.reject'{} | #'basic.recover_async'{} | #'basic.recover'{} | #'basic.recover_ok'{} | #'basic.nack'{} | #'tx.select'{} | #'tx.select_ok'{} | #'tx.commit'{} | #'tx.commit_ok'{} | #'tx.rollback'{} | #'tx.rollback_ok'{} | #'confirm.select'{} | #'confirm.select_ok'{} )). -type(amqp_method_field_name() :: ( queue | challenge | consumer_tag | realm | exclusive | passive | active | ticket | queue | write | exchange | read | ticket | exchange | routing_key | ticket | exchange | type | passive | durable | consumer_tag | auto_delete | no_local | internal | queue | requeue | nowait | arguments | nowait | routing_key | mandatory | exchange | ticket | no_ack | no_ack | nowait | exchange | if_unused | nowait | reply_text | exchange | ticket | frame_max | exclusive | destination | consumer_tag | source | reply_code | routing_key | nowait | ticket | arguments | exchange | nowait | routing_key | nowait | ticket | destination | source | routing_key | nowait | arguments | arguments | delivery_tag | redelivered | exchange | routing_key | ticket | ticket | queue | passive | durable | exclusive | queue | ticket | auto_delete | nowait | arguments | delivery_tag | queue | queue | if_unused | message_count | consumer_count | requeue | version_major | version_minor | routing_key | server_properties | message_count | mechanisms | routing_key | nowait | client_properties | mechanism | locales | prefetch_count | response | nowait | locale | ticket | queue | response | consumer_tag | channel_max | requeue | message_count | heartbeat | channel_max | arguments | cluster_id | frame_max | consumer_tag | heartbeat | if_empty | virtual_host | capabilities | requeue | insist | delivery_tag | delivery_tag | message_count | known_hosts | reply_code | reply_text | class_id | multiple | method_id | redelivered | arguments | out_of_band | channel_id | delivery_tag | active | prefetch_size | active | global | multiple | reply_code | ticket | reply_text | immediate | class_id | method_id | ticket )). -type(amqp_property_record() :: ( #'P_connection'{} | #'P_channel'{} | #'P_access'{} | #'P_exchange'{} | #'P_queue'{} | #'P_basic'{} | #'P_tx'{} | #'P_confirm'{} )). -type(amqp_exception() :: ( 'frame_method' | 'frame_header' | 'frame_body' | 'frame_heartbeat' | 'frame_min_size' | 'frame_end' | 'reply_success' | 'content_too_large' | 'no_route' | 'no_consumers' | 'access_refused' | 'not_found' | 'resource_locked' | 'precondition_failed' | 'connection_forced' | 'invalid_path' | 'frame_error' | 'syntax_error' | 'command_invalid' | 'channel_error' | 'unexpected_frame' | 'resource_error' | 'not_allowed' | 'not_implemented' | 'internal_error' )). -type(amqp_exception_code() :: ( 1 | 2 | 3 | 8 | 4096 | 206 | 200 | 311 | 312 | 313 | 403 | 404 | 405 | 406 | 320 | 402 | 501 | 502 | 503 | 504 | 505 | 506 | 530 | 540 | 541 )). -type(amqp_class_id() :: ( 40 | 10 | 50 | 20 | 85 | 90 | 60 | 30 )). -type(amqp_class_name() :: ( 'connection' | 'channel' | 'access' | 'exchange' | 'queue' | 'basic' | 'tx' | 'confirm' )). -endif. % use_specs %% Method signatures -ifdef(use_specs). -spec(version/0 :: () -> {non_neg_integer(), non_neg_integer(), non_neg_integer()}). -spec(lookup_method_name/1 :: (amqp_method()) -> amqp_method_name()). -spec(lookup_class_name/1 :: (amqp_class_id()) -> amqp_class_name()). -spec(method_id/1 :: (amqp_method_name()) -> amqp_method()). -spec(method_has_content/1 :: (amqp_method_name()) -> boolean()). -spec(is_method_synchronous/1 :: (amqp_method_record()) -> boolean()). -spec(method_record/1 :: (amqp_method_name()) -> amqp_method_record()). -spec(method_fieldnames/1 :: (amqp_method_name()) -> [amqp_method_field_name()]). -spec(decode_method_fields/2 :: (amqp_method_name(), binary()) -> amqp_method_record() | rabbit_types:connection_exit()). -spec(decode_properties/2 :: (non_neg_integer(), binary()) -> amqp_property_record()). -spec(encode_method_fields/1 :: (amqp_method_record()) -> binary()). -spec(encode_properties/1 :: (amqp_property_record()) -> binary()). -spec(lookup_amqp_exception/1 :: (amqp_exception()) -> {boolean(), amqp_exception_code(), binary()}). -spec(amqp_exception/1 :: (amqp_exception_code()) -> amqp_exception()). -endif. % use_specs bitvalue(true) -> 1; bitvalue(false) -> 0; bitvalue(undefined) -> 0. shortstr_size(S) -> case size(S) of Len when Len =< 255 -> Len; _ -> exit(method_field_shortstr_overflow) end. -define(SHORTSTR_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(LONGSTR_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(SHORT_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(LONG_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(LONGLONG_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(OCTET_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(TABLE_VAL(R, L, V, X), begin <> = R, {rabbit_binary_parser:parse_table(V), X} end). -define(TIMESTAMP_VAL(R, L, V, X), begin <> = R, {V, X} end). -define(SHORTSTR_PROP(X, L), begin L = size(X), if L < 256 -> <>; true -> exit(content_properties_shortstr_overflow) end end). -define(LONGSTR_PROP(X, L), begin L = size(X), <> end). -define(OCTET_PROP(X, L), <>). -define(SHORT_PROP(X, L), <>). -define(LONG_PROP(X, L), <>). -define(LONGLONG_PROP(X, L), <>). -define(TIMESTAMP_PROP(X, L), <>). -define(TABLE_PROP(X, T), begin T = rabbit_binary_generator:generate_table(X), <<(size(T)):32, T/binary>> end). version() -> {0, 9, 1}. lookup_method_name({10, 10}) -> 'connection.start'; lookup_method_name({10, 11}) -> 'connection.start_ok'; lookup_method_name({10, 20}) -> 'connection.secure'; lookup_method_name({10, 21}) -> 'connection.secure_ok'; lookup_method_name({10, 30}) -> 'connection.tune'; lookup_method_name({10, 31}) -> 'connection.tune_ok'; lookup_method_name({10, 40}) -> 'connection.open'; lookup_method_name({10, 41}) -> 'connection.open_ok'; lookup_method_name({10, 50}) -> 'connection.close'; lookup_method_name({10, 51}) -> 'connection.close_ok'; lookup_method_name({20, 10}) -> 'channel.open'; lookup_method_name({20, 11}) -> 'channel.open_ok'; lookup_method_name({20, 20}) -> 'channel.flow'; lookup_method_name({20, 21}) -> 'channel.flow_ok'; lookup_method_name({20, 40}) -> 'channel.close'; lookup_method_name({20, 41}) -> 'channel.close_ok'; lookup_method_name({30, 10}) -> 'access.request'; lookup_method_name({30, 11}) -> 'access.request_ok'; lookup_method_name({40, 10}) -> 'exchange.declare'; lookup_method_name({40, 11}) -> 'exchange.declare_ok'; lookup_method_name({40, 20}) -> 'exchange.delete'; lookup_method_name({40, 21}) -> 'exchange.delete_ok'; lookup_method_name({40, 30}) -> 'exchange.bind'; lookup_method_name({40, 31}) -> 'exchange.bind_ok'; lookup_method_name({40, 40}) -> 'exchange.unbind'; lookup_method_name({40, 51}) -> 'exchange.unbind_ok'; lookup_method_name({50, 10}) -> 'queue.declare'; lookup_method_name({50, 11}) -> 'queue.declare_ok'; lookup_method_name({50, 20}) -> 'queue.bind'; lookup_method_name({50, 21}) -> 'queue.bind_ok'; lookup_method_name({50, 30}) -> 'queue.purge'; lookup_method_name({50, 31}) -> 'queue.purge_ok'; lookup_method_name({50, 40}) -> 'queue.delete'; lookup_method_name({50, 41}) -> 'queue.delete_ok'; lookup_method_name({50, 50}) -> 'queue.unbind'; lookup_method_name({50, 51}) -> 'queue.unbind_ok'; lookup_method_name({60, 10}) -> 'basic.qos'; lookup_method_name({60, 11}) -> 'basic.qos_ok'; lookup_method_name({60, 20}) -> 'basic.consume'; lookup_method_name({60, 21}) -> 'basic.consume_ok'; lookup_method_name({60, 30}) -> 'basic.cancel'; lookup_method_name({60, 31}) -> 'basic.cancel_ok'; lookup_method_name({60, 40}) -> 'basic.publish'; lookup_method_name({60, 50}) -> 'basic.return'; lookup_method_name({60, 60}) -> 'basic.deliver'; lookup_method_name({60, 70}) -> 'basic.get'; lookup_method_name({60, 71}) -> 'basic.get_ok'; lookup_method_name({60, 72}) -> 'basic.get_empty'; lookup_method_name({60, 80}) -> 'basic.ack'; lookup_method_name({60, 90}) -> 'basic.reject'; lookup_method_name({60, 100}) -> 'basic.recover_async'; lookup_method_name({60, 110}) -> 'basic.recover'; lookup_method_name({60, 111}) -> 'basic.recover_ok'; lookup_method_name({60, 120}) -> 'basic.nack'; lookup_method_name({90, 10}) -> 'tx.select'; lookup_method_name({90, 11}) -> 'tx.select_ok'; lookup_method_name({90, 20}) -> 'tx.commit'; lookup_method_name({90, 21}) -> 'tx.commit_ok'; lookup_method_name({90, 30}) -> 'tx.rollback'; lookup_method_name({90, 31}) -> 'tx.rollback_ok'; lookup_method_name({85, 10}) -> 'confirm.select'; lookup_method_name({85, 11}) -> 'confirm.select_ok'; lookup_method_name({_ClassId, _MethodId} = Id) -> exit({unknown_method_id, Id}). lookup_class_name(10) -> 'connection'; lookup_class_name(20) -> 'channel'; lookup_class_name(30) -> 'access'; lookup_class_name(40) -> 'exchange'; lookup_class_name(50) -> 'queue'; lookup_class_name(60) -> 'basic'; lookup_class_name(90) -> 'tx'; lookup_class_name(85) -> 'confirm'; lookup_class_name(ClassId) -> exit({unknown_class_id, ClassId}). method_id('connection.start') -> {10, 10}; method_id('connection.start_ok') -> {10, 11}; method_id('connection.secure') -> {10, 20}; method_id('connection.secure_ok') -> {10, 21}; method_id('connection.tune') -> {10, 30}; method_id('connection.tune_ok') -> {10, 31}; method_id('connection.open') -> {10, 40}; method_id('connection.open_ok') -> {10, 41}; method_id('connection.close') -> {10, 50}; method_id('connection.close_ok') -> {10, 51}; method_id('channel.open') -> {20, 10}; method_id('channel.open_ok') -> {20, 11}; method_id('channel.flow') -> {20, 20}; method_id('channel.flow_ok') -> {20, 21}; method_id('channel.close') -> {20, 40}; method_id('channel.close_ok') -> {20, 41}; method_id('access.request') -> {30, 10}; method_id('access.request_ok') -> {30, 11}; method_id('exchange.declare') -> {40, 10}; method_id('exchange.declare_ok') -> {40, 11}; method_id('exchange.delete') -> {40, 20}; method_id('exchange.delete_ok') -> {40, 21}; method_id('exchange.bind') -> {40, 30}; method_id('exchange.bind_ok') -> {40, 31}; method_id('exchange.unbind') -> {40, 40}; method_id('exchange.unbind_ok') -> {40, 51}; method_id('queue.declare') -> {50, 10}; method_id('queue.declare_ok') -> {50, 11}; method_id('queue.bind') -> {50, 20}; method_id('queue.bind_ok') -> {50, 21}; method_id('queue.purge') -> {50, 30}; method_id('queue.purge_ok') -> {50, 31}; method_id('queue.delete') -> {50, 40}; method_id('queue.delete_ok') -> {50, 41}; method_id('queue.unbind') -> {50, 50}; method_id('queue.unbind_ok') -> {50, 51}; method_id('basic.qos') -> {60, 10}; method_id('basic.qos_ok') -> {60, 11}; method_id('basic.consume') -> {60, 20}; method_id('basic.consume_ok') -> {60, 21}; method_id('basic.cancel') -> {60, 30}; method_id('basic.cancel_ok') -> {60, 31}; method_id('basic.publish') -> {60, 40}; method_id('basic.return') -> {60, 50}; method_id('basic.deliver') -> {60, 60}; method_id('basic.get') -> {60, 70}; method_id('basic.get_ok') -> {60, 71}; method_id('basic.get_empty') -> {60, 72}; method_id('basic.ack') -> {60, 80}; method_id('basic.reject') -> {60, 90}; method_id('basic.recover_async') -> {60, 100}; method_id('basic.recover') -> {60, 110}; method_id('basic.recover_ok') -> {60, 111}; method_id('basic.nack') -> {60, 120}; method_id('tx.select') -> {90, 10}; method_id('tx.select_ok') -> {90, 11}; method_id('tx.commit') -> {90, 20}; method_id('tx.commit_ok') -> {90, 21}; method_id('tx.rollback') -> {90, 30}; method_id('tx.rollback_ok') -> {90, 31}; method_id('confirm.select') -> {85, 10}; method_id('confirm.select_ok') -> {85, 11}; method_id(Name) -> exit({unknown_method_name, Name}). method_has_content('connection.start') -> false; method_has_content('connection.start_ok') -> false; method_has_content('connection.secure') -> false; method_has_content('connection.secure_ok') -> false; method_has_content('connection.tune') -> false; method_has_content('connection.tune_ok') -> false; method_has_content('connection.open') -> false; method_has_content('connection.open_ok') -> false; method_has_content('connection.close') -> false; method_has_content('connection.close_ok') -> false; method_has_content('channel.open') -> false; method_has_content('channel.open_ok') -> false; method_has_content('channel.flow') -> false; method_has_content('channel.flow_ok') -> false; method_has_content('channel.close') -> false; method_has_content('channel.close_ok') -> false; method_has_content('access.request') -> false; method_has_content('access.request_ok') -> false; method_has_content('exchange.declare') -> false; method_has_content('exchange.declare_ok') -> false; method_has_content('exchange.delete') -> false; method_has_content('exchange.delete_ok') -> false; method_has_content('exchange.bind') -> false; method_has_content('exchange.bind_ok') -> false; method_has_content('exchange.unbind') -> false; method_has_content('exchange.unbind_ok') -> false; method_has_content('queue.declare') -> false; method_has_content('queue.declare_ok') -> false; method_has_content('queue.bind') -> false; method_has_content('queue.bind_ok') -> false; method_has_content('queue.purge') -> false; method_has_content('queue.purge_ok') -> false; method_has_content('queue.delete') -> false; method_has_content('queue.delete_ok') -> false; method_has_content('queue.unbind') -> false; method_has_content('queue.unbind_ok') -> false; method_has_content('basic.qos') -> false; method_has_content('basic.qos_ok') -> false; method_has_content('basic.consume') -> false; method_has_content('basic.consume_ok') -> false; method_has_content('basic.cancel') -> false; method_has_content('basic.cancel_ok') -> false; method_has_content('basic.publish') -> true; method_has_content('basic.return') -> true; method_has_content('basic.deliver') -> true; method_has_content('basic.get') -> false; method_has_content('basic.get_ok') -> true; method_has_content('basic.get_empty') -> false; method_has_content('basic.ack') -> false; method_has_content('basic.reject') -> false; method_has_content('basic.recover_async') -> false; method_has_content('basic.recover') -> false; method_has_content('basic.recover_ok') -> false; method_has_content('basic.nack') -> false; method_has_content('tx.select') -> false; method_has_content('tx.select_ok') -> false; method_has_content('tx.commit') -> false; method_has_content('tx.commit_ok') -> false; method_has_content('tx.rollback') -> false; method_has_content('tx.rollback_ok') -> false; method_has_content('confirm.select') -> false; method_has_content('confirm.select_ok') -> false; method_has_content(Name) -> exit({unknown_method_name, Name}). is_method_synchronous(#'connection.start'{}) -> true; is_method_synchronous(#'connection.start_ok'{}) -> false; is_method_synchronous(#'connection.secure'{}) -> true; is_method_synchronous(#'connection.secure_ok'{}) -> false; is_method_synchronous(#'connection.tune'{}) -> true; is_method_synchronous(#'connection.tune_ok'{}) -> false; is_method_synchronous(#'connection.open'{}) -> true; is_method_synchronous(#'connection.open_ok'{}) -> false; is_method_synchronous(#'connection.close'{}) -> true; is_method_synchronous(#'connection.close_ok'{}) -> false; is_method_synchronous(#'channel.open'{}) -> true; is_method_synchronous(#'channel.open_ok'{}) -> false; is_method_synchronous(#'channel.flow'{}) -> true; is_method_synchronous(#'channel.flow_ok'{}) -> false; is_method_synchronous(#'channel.close'{}) -> true; is_method_synchronous(#'channel.close_ok'{}) -> false; is_method_synchronous(#'access.request'{}) -> true; is_method_synchronous(#'access.request_ok'{}) -> false; is_method_synchronous(#'exchange.declare'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'exchange.declare_ok'{}) -> false; is_method_synchronous(#'exchange.delete'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'exchange.delete_ok'{}) -> false; is_method_synchronous(#'exchange.bind'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'exchange.bind_ok'{}) -> false; is_method_synchronous(#'exchange.unbind'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'exchange.unbind_ok'{}) -> false; is_method_synchronous(#'queue.declare'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'queue.declare_ok'{}) -> false; is_method_synchronous(#'queue.bind'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'queue.bind_ok'{}) -> false; is_method_synchronous(#'queue.purge'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'queue.purge_ok'{}) -> false; is_method_synchronous(#'queue.delete'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'queue.delete_ok'{}) -> false; is_method_synchronous(#'queue.unbind'{}) -> true; is_method_synchronous(#'queue.unbind_ok'{}) -> false; is_method_synchronous(#'basic.qos'{}) -> true; is_method_synchronous(#'basic.qos_ok'{}) -> false; is_method_synchronous(#'basic.consume'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'basic.consume_ok'{}) -> false; is_method_synchronous(#'basic.cancel'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'basic.cancel_ok'{}) -> false; is_method_synchronous(#'basic.publish'{}) -> false; is_method_synchronous(#'basic.return'{}) -> false; is_method_synchronous(#'basic.deliver'{}) -> false; is_method_synchronous(#'basic.get'{}) -> true; is_method_synchronous(#'basic.get_ok'{}) -> false; is_method_synchronous(#'basic.get_empty'{}) -> false; is_method_synchronous(#'basic.ack'{}) -> false; is_method_synchronous(#'basic.reject'{}) -> false; is_method_synchronous(#'basic.recover_async'{}) -> false; is_method_synchronous(#'basic.recover'{}) -> true; is_method_synchronous(#'basic.recover_ok'{}) -> false; is_method_synchronous(#'basic.nack'{}) -> false; is_method_synchronous(#'tx.select'{}) -> true; is_method_synchronous(#'tx.select_ok'{}) -> false; is_method_synchronous(#'tx.commit'{}) -> true; is_method_synchronous(#'tx.commit_ok'{}) -> false; is_method_synchronous(#'tx.rollback'{}) -> true; is_method_synchronous(#'tx.rollback_ok'{}) -> false; is_method_synchronous(#'confirm.select'{nowait = NoWait}) -> not(NoWait); is_method_synchronous(#'confirm.select_ok'{}) -> false; is_method_synchronous(Name) -> exit({unknown_method_name, Name}). method_record('connection.start') -> #'connection.start'{}; method_record('connection.start_ok') -> #'connection.start_ok'{}; method_record('connection.secure') -> #'connection.secure'{}; method_record('connection.secure_ok') -> #'connection.secure_ok'{}; method_record('connection.tune') -> #'connection.tune'{}; method_record('connection.tune_ok') -> #'connection.tune_ok'{}; method_record('connection.open') -> #'connection.open'{}; method_record('connection.open_ok') -> #'connection.open_ok'{}; method_record('connection.close') -> #'connection.close'{}; method_record('connection.close_ok') -> #'connection.close_ok'{}; method_record('channel.open') -> #'channel.open'{}; method_record('channel.open_ok') -> #'channel.open_ok'{}; method_record('channel.flow') -> #'channel.flow'{}; method_record('channel.flow_ok') -> #'channel.flow_ok'{}; method_record('channel.close') -> #'channel.close'{}; method_record('channel.close_ok') -> #'channel.close_ok'{}; method_record('access.request') -> #'access.request'{}; method_record('access.request_ok') -> #'access.request_ok'{}; method_record('exchange.declare') -> #'exchange.declare'{}; method_record('exchange.declare_ok') -> #'exchange.declare_ok'{}; method_record('exchange.delete') -> #'exchange.delete'{}; method_record('exchange.delete_ok') -> #'exchange.delete_ok'{}; method_record('exchange.bind') -> #'exchange.bind'{}; method_record('exchange.bind_ok') -> #'exchange.bind_ok'{}; method_record('exchange.unbind') -> #'exchange.unbind'{}; method_record('exchange.unbind_ok') -> #'exchange.unbind_ok'{}; method_record('queue.declare') -> #'queue.declare'{}; method_record('queue.declare_ok') -> #'queue.declare_ok'{}; method_record('queue.bind') -> #'queue.bind'{}; method_record('queue.bind_ok') -> #'queue.bind_ok'{}; method_record('queue.purge') -> #'queue.purge'{}; method_record('queue.purge_ok') -> #'queue.purge_ok'{}; method_record('queue.delete') -> #'queue.delete'{}; method_record('queue.delete_ok') -> #'queue.delete_ok'{}; method_record('queue.unbind') -> #'queue.unbind'{}; method_record('queue.unbind_ok') -> #'queue.unbind_ok'{}; method_record('basic.qos') -> #'basic.qos'{}; method_record('basic.qos_ok') -> #'basic.qos_ok'{}; method_record('basic.consume') -> #'basic.consume'{}; method_record('basic.consume_ok') -> #'basic.consume_ok'{}; method_record('basic.cancel') -> #'basic.cancel'{}; method_record('basic.cancel_ok') -> #'basic.cancel_ok'{}; method_record('basic.publish') -> #'basic.publish'{}; method_record('basic.return') -> #'basic.return'{}; method_record('basic.deliver') -> #'basic.deliver'{}; method_record('basic.get') -> #'basic.get'{}; method_record('basic.get_ok') -> #'basic.get_ok'{}; method_record('basic.get_empty') -> #'basic.get_empty'{}; method_record('basic.ack') -> #'basic.ack'{}; method_record('basic.reject') -> #'basic.reject'{}; method_record('basic.recover_async') -> #'basic.recover_async'{}; method_record('basic.recover') -> #'basic.recover'{}; method_record('basic.recover_ok') -> #'basic.recover_ok'{}; method_record('basic.nack') -> #'basic.nack'{}; method_record('tx.select') -> #'tx.select'{}; method_record('tx.select_ok') -> #'tx.select_ok'{}; method_record('tx.commit') -> #'tx.commit'{}; method_record('tx.commit_ok') -> #'tx.commit_ok'{}; method_record('tx.rollback') -> #'tx.rollback'{}; method_record('tx.rollback_ok') -> #'tx.rollback_ok'{}; method_record('confirm.select') -> #'confirm.select'{}; method_record('confirm.select_ok') -> #'confirm.select_ok'{}; method_record(Name) -> exit({unknown_method_name, Name}). method_fieldnames('connection.start') -> [version_major, version_minor, server_properties, mechanisms, locales]; method_fieldnames('connection.start_ok') -> [client_properties, mechanism, response, locale]; method_fieldnames('connection.secure') -> [challenge]; method_fieldnames('connection.secure_ok') -> [response]; method_fieldnames('connection.tune') -> [channel_max, frame_max, heartbeat]; method_fieldnames('connection.tune_ok') -> [channel_max, frame_max, heartbeat]; method_fieldnames('connection.open') -> [virtual_host, capabilities, insist]; method_fieldnames('connection.open_ok') -> [known_hosts]; method_fieldnames('connection.close') -> [reply_code, reply_text, class_id, method_id]; method_fieldnames('connection.close_ok') -> []; method_fieldnames('channel.open') -> [out_of_band]; method_fieldnames('channel.open_ok') -> [channel_id]; method_fieldnames('channel.flow') -> [active]; method_fieldnames('channel.flow_ok') -> [active]; method_fieldnames('channel.close') -> [reply_code, reply_text, class_id, method_id]; method_fieldnames('channel.close_ok') -> []; method_fieldnames('access.request') -> [realm, exclusive, passive, active, write, read]; method_fieldnames('access.request_ok') -> [ticket]; method_fieldnames('exchange.declare') -> [ticket, exchange, type, passive, durable, auto_delete, internal, nowait, arguments]; method_fieldnames('exchange.declare_ok') -> []; method_fieldnames('exchange.delete') -> [ticket, exchange, if_unused, nowait]; method_fieldnames('exchange.delete_ok') -> []; method_fieldnames('exchange.bind') -> [ticket, destination, source, routing_key, nowait, arguments]; method_fieldnames('exchange.bind_ok') -> []; method_fieldnames('exchange.unbind') -> [ticket, destination, source, routing_key, nowait, arguments]; method_fieldnames('exchange.unbind_ok') -> []; method_fieldnames('queue.declare') -> [ticket, queue, passive, durable, exclusive, auto_delete, nowait, arguments]; method_fieldnames('queue.declare_ok') -> [queue, message_count, consumer_count]; method_fieldnames('queue.bind') -> [ticket, queue, exchange, routing_key, nowait, arguments]; method_fieldnames('queue.bind_ok') -> []; method_fieldnames('queue.purge') -> [ticket, queue, nowait]; method_fieldnames('queue.purge_ok') -> [message_count]; method_fieldnames('queue.delete') -> [ticket, queue, if_unused, if_empty, nowait]; method_fieldnames('queue.delete_ok') -> [message_count]; method_fieldnames('queue.unbind') -> [ticket, queue, exchange, routing_key, arguments]; method_fieldnames('queue.unbind_ok') -> []; method_fieldnames('basic.qos') -> [prefetch_size, prefetch_count, global]; method_fieldnames('basic.qos_ok') -> []; method_fieldnames('basic.consume') -> [ticket, queue, consumer_tag, no_local, no_ack, exclusive, nowait, arguments]; method_fieldnames('basic.consume_ok') -> [consumer_tag]; method_fieldnames('basic.cancel') -> [consumer_tag, nowait]; method_fieldnames('basic.cancel_ok') -> [consumer_tag]; method_fieldnames('basic.publish') -> [ticket, exchange, routing_key, mandatory, immediate]; method_fieldnames('basic.return') -> [reply_code, reply_text, exchange, routing_key]; method_fieldnames('basic.deliver') -> [consumer_tag, delivery_tag, redelivered, exchange, routing_key]; method_fieldnames('basic.get') -> [ticket, queue, no_ack]; method_fieldnames('basic.get_ok') -> [delivery_tag, redelivered, exchange, routing_key, message_count]; method_fieldnames('basic.get_empty') -> [cluster_id]; method_fieldnames('basic.ack') -> [delivery_tag, multiple]; method_fieldnames('basic.reject') -> [delivery_tag, requeue]; method_fieldnames('basic.recover_async') -> [requeue]; method_fieldnames('basic.recover') -> [requeue]; method_fieldnames('basic.recover_ok') -> []; method_fieldnames('basic.nack') -> [delivery_tag, multiple, requeue]; method_fieldnames('tx.select') -> []; method_fieldnames('tx.select_ok') -> []; method_fieldnames('tx.commit') -> []; method_fieldnames('tx.commit_ok') -> []; method_fieldnames('tx.rollback') -> []; method_fieldnames('tx.rollback_ok') -> []; method_fieldnames('confirm.select') -> [nowait]; method_fieldnames('confirm.select_ok') -> []; method_fieldnames(Name) -> exit({unknown_method_name, Name}). decode_method_fields('connection.start', <>) -> F2 = rabbit_binary_parser:parse_table(F2Tab), #'connection.start'{version_major = F0, version_minor = F1, server_properties = F2, mechanisms = F3, locales = F4}; decode_method_fields('connection.start_ok', <>) -> F0 = rabbit_binary_parser:parse_table(F0Tab), #'connection.start_ok'{client_properties = F0, mechanism = F1, response = F2, locale = F3}; decode_method_fields('connection.secure', <>) -> #'connection.secure'{challenge = F0}; decode_method_fields('connection.secure_ok', <>) -> #'connection.secure_ok'{response = F0}; decode_method_fields('connection.tune', <>) -> #'connection.tune'{channel_max = F0, frame_max = F1, heartbeat = F2}; decode_method_fields('connection.tune_ok', <>) -> #'connection.tune_ok'{channel_max = F0, frame_max = F1, heartbeat = F2}; decode_method_fields('connection.open', <>) -> F2 = ((F2Bits band 1) /= 0), #'connection.open'{virtual_host = F0, capabilities = F1, insist = F2}; decode_method_fields('connection.open_ok', <>) -> #'connection.open_ok'{known_hosts = F0}; decode_method_fields('connection.close', <>) -> #'connection.close'{reply_code = F0, reply_text = F1, class_id = F2, method_id = F3}; decode_method_fields('connection.close_ok', <<>>) -> #'connection.close_ok'{}; decode_method_fields('channel.open', <>) -> #'channel.open'{out_of_band = F0}; decode_method_fields('channel.open_ok', <>) -> #'channel.open_ok'{channel_id = F0}; decode_method_fields('channel.flow', <>) -> F0 = ((F0Bits band 1) /= 0), #'channel.flow'{active = F0}; decode_method_fields('channel.flow_ok', <>) -> F0 = ((F0Bits band 1) /= 0), #'channel.flow_ok'{active = F0}; decode_method_fields('channel.close', <>) -> #'channel.close'{reply_code = F0, reply_text = F1, class_id = F2, method_id = F3}; decode_method_fields('channel.close_ok', <<>>) -> #'channel.close_ok'{}; decode_method_fields('access.request', <>) -> F1 = ((F1Bits band 1) /= 0), F2 = ((F1Bits band 2) /= 0), F3 = ((F1Bits band 4) /= 0), F4 = ((F1Bits band 8) /= 0), F5 = ((F1Bits band 16) /= 0), #'access.request'{realm = F0, exclusive = F1, passive = F2, active = F3, write = F4, read = F5}; decode_method_fields('access.request_ok', <>) -> #'access.request_ok'{ticket = F0}; decode_method_fields('exchange.declare', <>) -> F3 = ((F3Bits band 1) /= 0), F4 = ((F3Bits band 2) /= 0), F5 = ((F3Bits band 4) /= 0), F6 = ((F3Bits band 8) /= 0), F7 = ((F3Bits band 16) /= 0), F8 = rabbit_binary_parser:parse_table(F8Tab), #'exchange.declare'{ticket = F0, exchange = F1, type = F2, passive = F3, durable = F4, auto_delete = F5, internal = F6, nowait = F7, arguments = F8}; decode_method_fields('exchange.declare_ok', <<>>) -> #'exchange.declare_ok'{}; decode_method_fields('exchange.delete', <>) -> F2 = ((F2Bits band 1) /= 0), F3 = ((F2Bits band 2) /= 0), #'exchange.delete'{ticket = F0, exchange = F1, if_unused = F2, nowait = F3}; decode_method_fields('exchange.delete_ok', <<>>) -> #'exchange.delete_ok'{}; decode_method_fields('exchange.bind', <>) -> F4 = ((F4Bits band 1) /= 0), F5 = rabbit_binary_parser:parse_table(F5Tab), #'exchange.bind'{ticket = F0, destination = F1, source = F2, routing_key = F3, nowait = F4, arguments = F5}; decode_method_fields('exchange.bind_ok', <<>>) -> #'exchange.bind_ok'{}; decode_method_fields('exchange.unbind', <>) -> F4 = ((F4Bits band 1) /= 0), F5 = rabbit_binary_parser:parse_table(F5Tab), #'exchange.unbind'{ticket = F0, destination = F1, source = F2, routing_key = F3, nowait = F4, arguments = F5}; decode_method_fields('exchange.unbind_ok', <<>>) -> #'exchange.unbind_ok'{}; decode_method_fields('queue.declare', <>) -> F2 = ((F2Bits band 1) /= 0), F3 = ((F2Bits band 2) /= 0), F4 = ((F2Bits band 4) /= 0), F5 = ((F2Bits band 8) /= 0), F6 = ((F2Bits band 16) /= 0), F7 = rabbit_binary_parser:parse_table(F7Tab), #'queue.declare'{ticket = F0, queue = F1, passive = F2, durable = F3, exclusive = F4, auto_delete = F5, nowait = F6, arguments = F7}; decode_method_fields('queue.declare_ok', <>) -> #'queue.declare_ok'{queue = F0, message_count = F1, consumer_count = F2}; decode_method_fields('queue.bind', <>) -> F4 = ((F4Bits band 1) /= 0), F5 = rabbit_binary_parser:parse_table(F5Tab), #'queue.bind'{ticket = F0, queue = F1, exchange = F2, routing_key = F3, nowait = F4, arguments = F5}; decode_method_fields('queue.bind_ok', <<>>) -> #'queue.bind_ok'{}; decode_method_fields('queue.purge', <>) -> F2 = ((F2Bits band 1) /= 0), #'queue.purge'{ticket = F0, queue = F1, nowait = F2}; decode_method_fields('queue.purge_ok', <>) -> #'queue.purge_ok'{message_count = F0}; decode_method_fields('queue.delete', <>) -> F2 = ((F2Bits band 1) /= 0), F3 = ((F2Bits band 2) /= 0), F4 = ((F2Bits band 4) /= 0), #'queue.delete'{ticket = F0, queue = F1, if_unused = F2, if_empty = F3, nowait = F4}; decode_method_fields('queue.delete_ok', <>) -> #'queue.delete_ok'{message_count = F0}; decode_method_fields('queue.unbind', <>) -> F4 = rabbit_binary_parser:parse_table(F4Tab), #'queue.unbind'{ticket = F0, queue = F1, exchange = F2, routing_key = F3, arguments = F4}; decode_method_fields('queue.unbind_ok', <<>>) -> #'queue.unbind_ok'{}; decode_method_fields('basic.qos', <>) -> F2 = ((F2Bits band 1) /= 0), #'basic.qos'{prefetch_size = F0, prefetch_count = F1, global = F2}; decode_method_fields('basic.qos_ok', <<>>) -> #'basic.qos_ok'{}; decode_method_fields('basic.consume', <>) -> F3 = ((F3Bits band 1) /= 0), F4 = ((F3Bits band 2) /= 0), F5 = ((F3Bits band 4) /= 0), F6 = ((F3Bits band 8) /= 0), F7 = rabbit_binary_parser:parse_table(F7Tab), #'basic.consume'{ticket = F0, queue = F1, consumer_tag = F2, no_local = F3, no_ack = F4, exclusive = F5, nowait = F6, arguments = F7}; decode_method_fields('basic.consume_ok', <>) -> #'basic.consume_ok'{consumer_tag = F0}; decode_method_fields('basic.cancel', <>) -> F1 = ((F1Bits band 1) /= 0), #'basic.cancel'{consumer_tag = F0, nowait = F1}; decode_method_fields('basic.cancel_ok', <>) -> #'basic.cancel_ok'{consumer_tag = F0}; decode_method_fields('basic.publish', <>) -> F3 = ((F3Bits band 1) /= 0), F4 = ((F3Bits band 2) /= 0), #'basic.publish'{ticket = F0, exchange = F1, routing_key = F2, mandatory = F3, immediate = F4}; decode_method_fields('basic.return', <>) -> #'basic.return'{reply_code = F0, reply_text = F1, exchange = F2, routing_key = F3}; decode_method_fields('basic.deliver', <>) -> F2 = ((F2Bits band 1) /= 0), #'basic.deliver'{consumer_tag = F0, delivery_tag = F1, redelivered = F2, exchange = F3, routing_key = F4}; decode_method_fields('basic.get', <>) -> F2 = ((F2Bits band 1) /= 0), #'basic.get'{ticket = F0, queue = F1, no_ack = F2}; decode_method_fields('basic.get_ok', <>) -> F1 = ((F1Bits band 1) /= 0), #'basic.get_ok'{delivery_tag = F0, redelivered = F1, exchange = F2, routing_key = F3, message_count = F4}; decode_method_fields('basic.get_empty', <>) -> #'basic.get_empty'{cluster_id = F0}; decode_method_fields('basic.ack', <>) -> F1 = ((F1Bits band 1) /= 0), #'basic.ack'{delivery_tag = F0, multiple = F1}; decode_method_fields('basic.reject', <>) -> F1 = ((F1Bits band 1) /= 0), #'basic.reject'{delivery_tag = F0, requeue = F1}; decode_method_fields('basic.recover_async', <>) -> F0 = ((F0Bits band 1) /= 0), #'basic.recover_async'{requeue = F0}; decode_method_fields('basic.recover', <>) -> F0 = ((F0Bits band 1) /= 0), #'basic.recover'{requeue = F0}; decode_method_fields('basic.recover_ok', <<>>) -> #'basic.recover_ok'{}; decode_method_fields('basic.nack', <>) -> F1 = ((F1Bits band 1) /= 0), F2 = ((F1Bits band 2) /= 0), #'basic.nack'{delivery_tag = F0, multiple = F1, requeue = F2}; decode_method_fields('tx.select', <<>>) -> #'tx.select'{}; decode_method_fields('tx.select_ok', <<>>) -> #'tx.select_ok'{}; decode_method_fields('tx.commit', <<>>) -> #'tx.commit'{}; decode_method_fields('tx.commit_ok', <<>>) -> #'tx.commit_ok'{}; decode_method_fields('tx.rollback', <<>>) -> #'tx.rollback'{}; decode_method_fields('tx.rollback_ok', <<>>) -> #'tx.rollback_ok'{}; decode_method_fields('confirm.select', <>) -> F0 = ((F0Bits band 1) /= 0), #'confirm.select'{nowait = F0}; decode_method_fields('confirm.select_ok', <<>>) -> #'confirm.select_ok'{}; decode_method_fields(Name, BinaryFields) -> rabbit_misc:frame_error(Name, BinaryFields). decode_properties(10, <<>>) -> #'P_connection'{}; decode_properties(20, <<>>) -> #'P_channel'{}; decode_properties(30, <<>>) -> #'P_access'{}; decode_properties(40, <<>>) -> #'P_exchange'{}; decode_properties(50, <<>>) -> #'P_queue'{}; decode_properties(60, <>) -> {F0, R1} = if P0 =:= 0 -> {undefined, R0}; true -> ?SHORTSTR_VAL(R0, L0, V0, X0) end, {F1, R2} = if P1 =:= 0 -> {undefined, R1}; true -> ?SHORTSTR_VAL(R1, L1, V1, X1) end, {F2, R3} = if P2 =:= 0 -> {undefined, R2}; true -> ?TABLE_VAL(R2, L2, V2, X2) end, {F3, R4} = if P3 =:= 0 -> {undefined, R3}; true -> ?OCTET_VAL(R3, L3, V3, X3) end, {F4, R5} = if P4 =:= 0 -> {undefined, R4}; true -> ?OCTET_VAL(R4, L4, V4, X4) end, {F5, R6} = if P5 =:= 0 -> {undefined, R5}; true -> ?SHORTSTR_VAL(R5, L5, V5, X5) end, {F6, R7} = if P6 =:= 0 -> {undefined, R6}; true -> ?SHORTSTR_VAL(R6, L6, V6, X6) end, {F7, R8} = if P7 =:= 0 -> {undefined, R7}; true -> ?SHORTSTR_VAL(R7, L7, V7, X7) end, {F8, R9} = if P8 =:= 0 -> {undefined, R8}; true -> ?SHORTSTR_VAL(R8, L8, V8, X8) end, {F9, R10} = if P9 =:= 0 -> {undefined, R9}; true -> ?TIMESTAMP_VAL(R9, L9, V9, X9) end, {F10, R11} = if P10 =:= 0 -> {undefined, R10}; true -> ?SHORTSTR_VAL(R10, L10, V10, X10) end, {F11, R12} = if P11 =:= 0 -> {undefined, R11}; true -> ?SHORTSTR_VAL(R11, L11, V11, X11) end, {F12, R13} = if P12 =:= 0 -> {undefined, R12}; true -> ?SHORTSTR_VAL(R12, L12, V12, X12) end, {F13, R14} = if P13 =:= 0 -> {undefined, R13}; true -> ?SHORTSTR_VAL(R13, L13, V13, X13) end, <<>> = R14, #'P_basic'{content_type = F0, content_encoding = F1, headers = F2, delivery_mode = F3, priority = F4, correlation_id = F5, reply_to = F6, expiration = F7, message_id = F8, timestamp = F9, type = F10, user_id = F11, app_id = F12, cluster_id = F13}; decode_properties(90, <<>>) -> #'P_tx'{}; decode_properties(85, <<>>) -> #'P_confirm'{}; decode_properties(ClassId, _BinaryFields) -> exit({unknown_class_id, ClassId}). encode_method_fields(#'connection.start'{version_major = F0, version_minor = F1, server_properties = F2, mechanisms = F3, locales = F4}) -> F2Tab = rabbit_binary_generator:generate_table(F2), F2Len = size(F2Tab), F3Len = size(F3), F4Len = size(F4), <>; encode_method_fields(#'connection.start_ok'{client_properties = F0, mechanism = F1, response = F2, locale = F3}) -> F0Tab = rabbit_binary_generator:generate_table(F0), F0Len = size(F0Tab), F1Len = shortstr_size(F1), F2Len = size(F2), F3Len = shortstr_size(F3), <>; encode_method_fields(#'connection.secure'{challenge = F0}) -> F0Len = size(F0), <>; encode_method_fields(#'connection.secure_ok'{response = F0}) -> F0Len = size(F0), <>; encode_method_fields(#'connection.tune'{channel_max = F0, frame_max = F1, heartbeat = F2}) -> <>; encode_method_fields(#'connection.tune_ok'{channel_max = F0, frame_max = F1, heartbeat = F2}) -> <>; encode_method_fields(#'connection.open'{virtual_host = F0, capabilities = F1, insist = F2}) -> F0Len = shortstr_size(F0), F1Len = shortstr_size(F1), F2Bits = ((bitvalue(F2) bsl 0)), <>; encode_method_fields(#'connection.open_ok'{known_hosts = F0}) -> F0Len = shortstr_size(F0), <>; encode_method_fields(#'connection.close'{reply_code = F0, reply_text = F1, class_id = F2, method_id = F3}) -> F1Len = shortstr_size(F1), <>; encode_method_fields(#'connection.close_ok'{}) -> <<>>; encode_method_fields(#'channel.open'{out_of_band = F0}) -> F0Len = shortstr_size(F0), <>; encode_method_fields(#'channel.open_ok'{channel_id = F0}) -> F0Len = size(F0), <>; encode_method_fields(#'channel.flow'{active = F0}) -> F0Bits = ((bitvalue(F0) bsl 0)), <>; encode_method_fields(#'channel.flow_ok'{active = F0}) -> F0Bits = ((bitvalue(F0) bsl 0)), <>; encode_method_fields(#'channel.close'{reply_code = F0, reply_text = F1, class_id = F2, method_id = F3}) -> F1Len = shortstr_size(F1), <>; encode_method_fields(#'channel.close_ok'{}) -> <<>>; encode_method_fields(#'access.request'{realm = F0, exclusive = F1, passive = F2, active = F3, write = F4, read = F5}) -> F0Len = shortstr_size(F0), F1Bits = ((bitvalue(F1) bsl 0) bor (bitvalue(F2) bsl 1) bor (bitvalue(F3) bsl 2) bor (bitvalue(F4) bsl 3) bor (bitvalue(F5) bsl 4)), <>; encode_method_fields(#'access.request_ok'{ticket = F0}) -> <>; encode_method_fields(#'exchange.declare'{ticket = F0, exchange = F1, type = F2, passive = F3, durable = F4, auto_delete = F5, internal = F6, nowait = F7, arguments = F8}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Bits = ((bitvalue(F3) bsl 0) bor (bitvalue(F4) bsl 1) bor (bitvalue(F5) bsl 2) bor (bitvalue(F6) bsl 3) bor (bitvalue(F7) bsl 4)), F8Tab = rabbit_binary_generator:generate_table(F8), F8Len = size(F8Tab), <>; encode_method_fields(#'exchange.declare_ok'{}) -> <<>>; encode_method_fields(#'exchange.delete'{ticket = F0, exchange = F1, if_unused = F2, nowait = F3}) -> F1Len = shortstr_size(F1), F2Bits = ((bitvalue(F2) bsl 0) bor (bitvalue(F3) bsl 1)), <>; encode_method_fields(#'exchange.delete_ok'{}) -> <<>>; encode_method_fields(#'exchange.bind'{ticket = F0, destination = F1, source = F2, routing_key = F3, nowait = F4, arguments = F5}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Len = shortstr_size(F3), F4Bits = ((bitvalue(F4) bsl 0)), F5Tab = rabbit_binary_generator:generate_table(F5), F5Len = size(F5Tab), <>; encode_method_fields(#'exchange.bind_ok'{}) -> <<>>; encode_method_fields(#'exchange.unbind'{ticket = F0, destination = F1, source = F2, routing_key = F3, nowait = F4, arguments = F5}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Len = shortstr_size(F3), F4Bits = ((bitvalue(F4) bsl 0)), F5Tab = rabbit_binary_generator:generate_table(F5), F5Len = size(F5Tab), <>; encode_method_fields(#'exchange.unbind_ok'{}) -> <<>>; encode_method_fields(#'queue.declare'{ticket = F0, queue = F1, passive = F2, durable = F3, exclusive = F4, auto_delete = F5, nowait = F6, arguments = F7}) -> F1Len = shortstr_size(F1), F2Bits = ((bitvalue(F2) bsl 0) bor (bitvalue(F3) bsl 1) bor (bitvalue(F4) bsl 2) bor (bitvalue(F5) bsl 3) bor (bitvalue(F6) bsl 4)), F7Tab = rabbit_binary_generator:generate_table(F7), F7Len = size(F7Tab), <>; encode_method_fields(#'queue.declare_ok'{queue = F0, message_count = F1, consumer_count = F2}) -> F0Len = shortstr_size(F0), <>; encode_method_fields(#'queue.bind'{ticket = F0, queue = F1, exchange = F2, routing_key = F3, nowait = F4, arguments = F5}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Len = shortstr_size(F3), F4Bits = ((bitvalue(F4) bsl 0)), F5Tab = rabbit_binary_generator:generate_table(F5), F5Len = size(F5Tab), <>; encode_method_fields(#'queue.bind_ok'{}) -> <<>>; encode_method_fields(#'queue.purge'{ticket = F0, queue = F1, nowait = F2}) -> F1Len = shortstr_size(F1), F2Bits = ((bitvalue(F2) bsl 0)), <>; encode_method_fields(#'queue.purge_ok'{message_count = F0}) -> <>; encode_method_fields(#'queue.delete'{ticket = F0, queue = F1, if_unused = F2, if_empty = F3, nowait = F4}) -> F1Len = shortstr_size(F1), F2Bits = ((bitvalue(F2) bsl 0) bor (bitvalue(F3) bsl 1) bor (bitvalue(F4) bsl 2)), <>; encode_method_fields(#'queue.delete_ok'{message_count = F0}) -> <>; encode_method_fields(#'queue.unbind'{ticket = F0, queue = F1, exchange = F2, routing_key = F3, arguments = F4}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Len = shortstr_size(F3), F4Tab = rabbit_binary_generator:generate_table(F4), F4Len = size(F4Tab), <>; encode_method_fields(#'queue.unbind_ok'{}) -> <<>>; encode_method_fields(#'basic.qos'{prefetch_size = F0, prefetch_count = F1, global = F2}) -> F2Bits = ((bitvalue(F2) bsl 0)), <>; encode_method_fields(#'basic.qos_ok'{}) -> <<>>; encode_method_fields(#'basic.consume'{ticket = F0, queue = F1, consumer_tag = F2, no_local = F3, no_ack = F4, exclusive = F5, nowait = F6, arguments = F7}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Bits = ((bitvalue(F3) bsl 0) bor (bitvalue(F4) bsl 1) bor (bitvalue(F5) bsl 2) bor (bitvalue(F6) bsl 3)), F7Tab = rabbit_binary_generator:generate_table(F7), F7Len = size(F7Tab), <>; encode_method_fields(#'basic.consume_ok'{consumer_tag = F0}) -> F0Len = shortstr_size(F0), <>; encode_method_fields(#'basic.cancel'{consumer_tag = F0, nowait = F1}) -> F0Len = shortstr_size(F0), F1Bits = ((bitvalue(F1) bsl 0)), <>; encode_method_fields(#'basic.cancel_ok'{consumer_tag = F0}) -> F0Len = shortstr_size(F0), <>; encode_method_fields(#'basic.publish'{ticket = F0, exchange = F1, routing_key = F2, mandatory = F3, immediate = F4}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Bits = ((bitvalue(F3) bsl 0) bor (bitvalue(F4) bsl 1)), <>; encode_method_fields(#'basic.return'{reply_code = F0, reply_text = F1, exchange = F2, routing_key = F3}) -> F1Len = shortstr_size(F1), F2Len = shortstr_size(F2), F3Len = shortstr_size(F3), <>; encode_method_fields(#'basic.deliver'{consumer_tag = F0, delivery_tag = F1, redelivered = F2, exchange = F3, routing_key = F4}) -> F0Len = shortstr_size(F0), F2Bits = ((bitvalue(F2) bsl 0)), F3Len = shortstr_size(F3), F4Len = shortstr_size(F4), <>; encode_method_fields(#'basic.get'{ticket = F0, queue = F1, no_ack = F2}) -> F1Len = shortstr_size(F1), F2Bits = ((bitvalue(F2) bsl 0)), <>; encode_method_fields(#'basic.get_ok'{delivery_tag = F0, redelivered = F1, exchange = F2, routing_key = F3, message_count = F4}) -> F1Bits = ((bitvalue(F1) bsl 0)), F2Len = shortstr_size(F2), F3Len = shortstr_size(F3), <>; encode_method_fields(#'basic.get_empty'{cluster_id = F0}) -> F0Len = shortstr_size(F0), <>; encode_method_fields(#'basic.ack'{delivery_tag = F0, multiple = F1}) -> F1Bits = ((bitvalue(F1) bsl 0)), <>; encode_method_fields(#'basic.reject'{delivery_tag = F0, requeue = F1}) -> F1Bits = ((bitvalue(F1) bsl 0)), <>; encode_method_fields(#'basic.recover_async'{requeue = F0}) -> F0Bits = ((bitvalue(F0) bsl 0)), <>; encode_method_fields(#'basic.recover'{requeue = F0}) -> F0Bits = ((bitvalue(F0) bsl 0)), <>; encode_method_fields(#'basic.recover_ok'{}) -> <<>>; encode_method_fields(#'basic.nack'{delivery_tag = F0, multiple = F1, requeue = F2}) -> F1Bits = ((bitvalue(F1) bsl 0) bor (bitvalue(F2) bsl 1)), <>; encode_method_fields(#'tx.select'{}) -> <<>>; encode_method_fields(#'tx.select_ok'{}) -> <<>>; encode_method_fields(#'tx.commit'{}) -> <<>>; encode_method_fields(#'tx.commit_ok'{}) -> <<>>; encode_method_fields(#'tx.rollback'{}) -> <<>>; encode_method_fields(#'tx.rollback_ok'{}) -> <<>>; encode_method_fields(#'confirm.select'{nowait = F0}) -> F0Bits = ((bitvalue(F0) bsl 0)), <>; encode_method_fields(#'confirm.select_ok'{}) -> <<>>; encode_method_fields(Record) -> exit({unknown_method_name, element(1, Record)}). encode_properties(#'P_connection'{}) -> <<>>; encode_properties(#'P_channel'{}) -> <<>>; encode_properties(#'P_access'{}) -> <<>>; encode_properties(#'P_exchange'{}) -> <<>>; encode_properties(#'P_queue'{}) -> <<>>; encode_properties(#'P_basic'{content_type = F0, content_encoding = F1, headers = F2, delivery_mode = F3, priority = F4, correlation_id = F5, reply_to = F6, expiration = F7, message_id = F8, timestamp = F9, type = F10, user_id = F11, app_id = F12, cluster_id = F13}) -> R0 = [<<>>], {P0, R1} = if F0 =:= undefined -> {0, R0}; true -> {1, [?SHORTSTR_PROP(F0, L0) | R0]} end, {P1, R2} = if F1 =:= undefined -> {0, R1}; true -> {1, [?SHORTSTR_PROP(F1, L1) | R1]} end, {P2, R3} = if F2 =:= undefined -> {0, R2}; true -> {1, [?TABLE_PROP(F2, L2) | R2]} end, {P3, R4} = if F3 =:= undefined -> {0, R3}; true -> {1, [?OCTET_PROP(F3, L3) | R3]} end, {P4, R5} = if F4 =:= undefined -> {0, R4}; true -> {1, [?OCTET_PROP(F4, L4) | R4]} end, {P5, R6} = if F5 =:= undefined -> {0, R5}; true -> {1, [?SHORTSTR_PROP(F5, L5) | R5]} end, {P6, R7} = if F6 =:= undefined -> {0, R6}; true -> {1, [?SHORTSTR_PROP(F6, L6) | R6]} end, {P7, R8} = if F7 =:= undefined -> {0, R7}; true -> {1, [?SHORTSTR_PROP(F7, L7) | R7]} end, {P8, R9} = if F8 =:= undefined -> {0, R8}; true -> {1, [?SHORTSTR_PROP(F8, L8) | R8]} end, {P9, R10} = if F9 =:= undefined -> {0, R9}; true -> {1, [?TIMESTAMP_PROP(F9, L9) | R9]} end, {P10, R11} = if F10 =:= undefined -> {0, R10}; true -> {1, [?SHORTSTR_PROP(F10, L10) | R10]} end, {P11, R12} = if F11 =:= undefined -> {0, R11}; true -> {1, [?SHORTSTR_PROP(F11, L11) | R11]} end, {P12, R13} = if F12 =:= undefined -> {0, R12}; true -> {1, [?SHORTSTR_PROP(F12, L12) | R12]} end, {P13, R14} = if F13 =:= undefined -> {0, R13}; true -> {1, [?SHORTSTR_PROP(F13, L13) | R13]} end, list_to_binary([<> | lists:reverse(R14)]); encode_properties(#'P_tx'{}) -> <<>>; encode_properties(#'P_confirm'{}) -> <<>>; encode_properties(Record) -> exit({unknown_properties_record, Record}). lookup_amqp_exception(content_too_large) -> {false, ?CONTENT_TOO_LARGE, <<"CONTENT_TOO_LARGE">>}; lookup_amqp_exception(no_route) -> {false, ?NO_ROUTE, <<"NO_ROUTE">>}; lookup_amqp_exception(no_consumers) -> {false, ?NO_CONSUMERS, <<"NO_CONSUMERS">>}; lookup_amqp_exception(access_refused) -> {false, ?ACCESS_REFUSED, <<"ACCESS_REFUSED">>}; lookup_amqp_exception(not_found) -> {false, ?NOT_FOUND, <<"NOT_FOUND">>}; lookup_amqp_exception(resource_locked) -> {false, ?RESOURCE_LOCKED, <<"RESOURCE_LOCKED">>}; lookup_amqp_exception(precondition_failed) -> {false, ?PRECONDITION_FAILED, <<"PRECONDITION_FAILED">>}; lookup_amqp_exception(connection_forced) -> {true, ?CONNECTION_FORCED, <<"CONNECTION_FORCED">>}; lookup_amqp_exception(invalid_path) -> {true, ?INVALID_PATH, <<"INVALID_PATH">>}; lookup_amqp_exception(frame_error) -> {true, ?FRAME_ERROR, <<"FRAME_ERROR">>}; lookup_amqp_exception(syntax_error) -> {true, ?SYNTAX_ERROR, <<"SYNTAX_ERROR">>}; lookup_amqp_exception(command_invalid) -> {true, ?COMMAND_INVALID, <<"COMMAND_INVALID">>}; lookup_amqp_exception(channel_error) -> {true, ?CHANNEL_ERROR, <<"CHANNEL_ERROR">>}; lookup_amqp_exception(unexpected_frame) -> {true, ?UNEXPECTED_FRAME, <<"UNEXPECTED_FRAME">>}; lookup_amqp_exception(resource_error) -> {true, ?RESOURCE_ERROR, <<"RESOURCE_ERROR">>}; lookup_amqp_exception(not_allowed) -> {true, ?NOT_ALLOWED, <<"NOT_ALLOWED">>}; lookup_amqp_exception(not_implemented) -> {true, ?NOT_IMPLEMENTED, <<"NOT_IMPLEMENTED">>}; lookup_amqp_exception(internal_error) -> {true, ?INTERNAL_ERROR, <<"INTERNAL_ERROR">>}; lookup_amqp_exception(Code) -> rabbit_log:warning("Unknown AMQP error code '~p'~n", [Code]), {true, ?INTERNAL_ERROR, <<"INTERNAL_ERROR">>}. amqp_exception(?FRAME_METHOD) -> frame_method; amqp_exception(?FRAME_HEADER) -> frame_header; amqp_exception(?FRAME_BODY) -> frame_body; amqp_exception(?FRAME_HEARTBEAT) -> frame_heartbeat; amqp_exception(?FRAME_MIN_SIZE) -> frame_min_size; amqp_exception(?FRAME_END) -> frame_end; amqp_exception(?REPLY_SUCCESS) -> reply_success; amqp_exception(?CONTENT_TOO_LARGE) -> content_too_large; amqp_exception(?NO_ROUTE) -> no_route; amqp_exception(?NO_CONSUMERS) -> no_consumers; amqp_exception(?ACCESS_REFUSED) -> access_refused; amqp_exception(?NOT_FOUND) -> not_found; amqp_exception(?RESOURCE_LOCKED) -> resource_locked; amqp_exception(?PRECONDITION_FAILED) -> precondition_failed; amqp_exception(?CONNECTION_FORCED) -> connection_forced; amqp_exception(?INVALID_PATH) -> invalid_path; amqp_exception(?FRAME_ERROR) -> frame_error; amqp_exception(?SYNTAX_ERROR) -> syntax_error; amqp_exception(?COMMAND_INVALID) -> command_invalid; amqp_exception(?CHANNEL_ERROR) -> channel_error; amqp_exception(?UNEXPECTED_FRAME) -> unexpected_frame; amqp_exception(?RESOURCE_ERROR) -> resource_error; amqp_exception(?NOT_ALLOWED) -> not_allowed; amqp_exception(?NOT_IMPLEMENTED) -> not_implemented; amqp_exception(?INTERNAL_ERROR) -> internal_error; amqp_exception(_Code) -> undefined. tsung-1.8.0/src/lib/rabbit_command_assembler.erl0000644000201100017670000001350314377756736021437 0ustar nniclausdream%% The contents of this file are subject to the Mozilla Public License %% Version 1.1 (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.mozilla.org/MPL/ %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and %% limitations under the License. %% %% The Original Code is RabbitMQ. %% %% The Initial Developer of the Original Code is VMware, Inc. %% Copyright (c) 2007-2012 VMware, Inc. All rights reserved. %% -module(rabbit_command_assembler). -include("rabbit_framing.hrl"). -include("rabbit.hrl"). -export([analyze_frame/3, init/1, process/2]). %%---------------------------------------------------------------------------- %%---------------------------------------------------------------------------- -ifdef(use_specs). -export_type([frame/0]). -type(frame_type() :: ?FRAME_METHOD | ?FRAME_HEADER | ?FRAME_BODY | ?FRAME_OOB_METHOD | ?FRAME_OOB_HEADER | ?FRAME_OOB_BODY | ?FRAME_TRACE | ?FRAME_HEARTBEAT). -type(protocol() :: rabbit_framing:protocol()). -type(method() :: rabbit_framing:amqp_method_record()). -type(class_id() :: rabbit_framing:amqp_class_id()). -type(weight() :: non_neg_integer()). -type(body_size() :: non_neg_integer()). -type(content() :: rabbit_types:undecoded_content()). -type(frame() :: {'method', rabbit_framing:amqp_method_name(), binary()} | {'content_header', class_id(), weight(), body_size(), binary()} | {'content_body', binary()}). -type(state() :: {'method', protocol()} | {'content_header', method(), class_id(), protocol()} | {'content_body', method(), body_size(), class_id(), protocol()}). -spec(analyze_frame/3 :: (frame_type(), binary(), protocol()) -> frame() | 'heartbeat' | 'error'). -spec(init/1 :: (protocol()) -> {ok, state()}). -spec(process/2 :: (frame(), state()) -> {ok, state()} | {ok, method(), state()} | {ok, method(), content(), state()} | {error, rabbit_types:amqp_error()}). -endif. %%-------------------------------------------------------------------- analyze_frame(?FRAME_METHOD, <>, Protocol) -> MethodName = Protocol:lookup_method_name({ClassId, MethodId}), {method, MethodName, MethodFields}; analyze_frame(?FRAME_HEADER, <>, _Protocol) -> {content_header, ClassId, Weight, BodySize, Properties}; analyze_frame(?FRAME_BODY, Body, _Protocol) -> {content_body, Body}; analyze_frame(?FRAME_HEARTBEAT, <<>>, _Protocol) -> heartbeat; analyze_frame(_Type, _Body, _Protocol) -> error. init(Protocol) -> {ok, {method, Protocol}}. process({method, MethodName, FieldsBin}, {method, Protocol}) -> try Method = Protocol:decode_method_fields(MethodName, FieldsBin), case Protocol:method_has_content(MethodName) of true -> {ClassId, _MethodId} = Protocol:method_id(MethodName), {ok, {content_header, Method, ClassId, Protocol}}; false -> {ok, Method, {method, Protocol}} end catch exit:#amqp_error{} = Reason -> {error, Reason} end; process(_Frame, {method, _Protocol}) -> unexpected_frame("expected method frame, " "got non method frame instead", [], none); process({content_header, ClassId, 0, 0, PropertiesBin}, {content_header, Method, ClassId, Protocol}) -> Content = empty_content(ClassId, PropertiesBin, Protocol), {ok, Method, Content, {method, Protocol}}; process({content_header, ClassId, 0, BodySize, PropertiesBin}, {content_header, Method, ClassId, Protocol}) -> Content = empty_content(ClassId, PropertiesBin, Protocol), {ok, {content_body, Method, BodySize, Content, Protocol}}; process({content_header, HeaderClassId, 0, _BodySize, _PropertiesBin}, {content_header, Method, ClassId, _Protocol}) -> unexpected_frame("expected content header for class ~w, " "got one for class ~w instead", [ClassId, HeaderClassId], Method); process(_Frame, {content_header, Method, ClassId, _Protocol}) -> unexpected_frame("expected content header for class ~w, " "got non content header frame instead", [ClassId], Method); process({content_body, FragmentBin}, {content_body, Method, RemainingSize, Content = #content{payload_fragments_rev = Fragments}, Protocol}) -> NewContent = Content#content{ payload_fragments_rev = [FragmentBin | Fragments]}, case RemainingSize - size(FragmentBin) of 0 -> {ok, Method, NewContent, {method, Protocol}}; Sz -> {ok, {content_body, Method, Sz, NewContent, Protocol}} end; process(_Frame, {content_body, Method, _RemainingSize, _Content, _Protocol}) -> unexpected_frame("expected content body, " "got non content body frame instead", [], Method). %%-------------------------------------------------------------------- empty_content(ClassId, PropertiesBin, Protocol) -> #content{class_id = ClassId, properties = none, properties_bin = PropertiesBin, protocol = Protocol, payload_fragments_rev = []}. unexpected_frame(Format, Params, Method) when is_atom(Method) -> {error, rabbit_misc:amqp_error(unexpected_frame, Format, Params, Method)}; unexpected_frame(Format, Params, Method) -> unexpected_frame(Format, Params, rabbit_misc:method_record_type(Method)). tsung-1.8.0/src/lib/rabbit_binary_parser.erl0000644000201100017670000000736114377756736020631 0ustar nniclausdream%% The contents of this file are subject to the Mozilla Public License %% Version 1.1 (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.mozilla.org/MPL/ %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and %% limitations under the License. %% %% The Original Code is RabbitMQ. %% %% The Initial Developer of the Original Code is VMware, Inc. %% Copyright (c) 2007-2012 VMware, Inc. All rights reserved. %% -module(rabbit_binary_parser). -include("rabbit.hrl"). -export([parse_table/1]). -export([ensure_content_decoded/1, clear_decoded_content/1]). %%---------------------------------------------------------------------------- -ifdef(use_specs). -spec(parse_table/1 :: (binary()) -> rabbit_framing:amqp_table()). -spec(ensure_content_decoded/1 :: (rabbit_types:content()) -> rabbit_types:decoded_content()). -spec(clear_decoded_content/1 :: (rabbit_types:content()) -> rabbit_types:undecoded_content()). -endif. %%---------------------------------------------------------------------------- %% parse_table supports the AMQP 0-8/0-9 standard types, S, I, D, T %% and F, as well as the QPid extensions b, d, f, l, s, t, x, and V. parse_table(<<>>) -> []; parse_table(<>) -> {Type, Value, Rest} = parse_field_value(ValueAndRest), [{NameString, Type, Value} | parse_table(Rest)]. parse_array(<<>>) -> []; parse_array(<>) -> {Type, Value, Rest} = parse_field_value(ValueAndRest), [{Type, Value} | parse_array(Rest)]. parse_field_value(<<"S", VLen:32/unsigned, V:VLen/binary, R/binary>>) -> {longstr, V, R}; parse_field_value(<<"I", V:32/signed, R/binary>>) -> {signedint, V, R}; parse_field_value(<<"D", Before:8/unsigned, After:32/unsigned, R/binary>>) -> {decimal, {Before, After}, R}; parse_field_value(<<"T", V:64/unsigned, R/binary>>) -> {timestamp, V, R}; parse_field_value(<<"F", VLen:32/unsigned, Table:VLen/binary, R/binary>>) -> {table, parse_table(Table), R}; parse_field_value(<<"A", VLen:32/unsigned, Array:VLen/binary, R/binary>>) -> {array, parse_array(Array), R}; parse_field_value(<<"b", V:8/unsigned, R/binary>>) -> {byte, V, R}; parse_field_value(<<"d", V:64/float, R/binary>>) -> {double, V, R}; parse_field_value(<<"f", V:32/float, R/binary>>) -> {float, V, R}; parse_field_value(<<"l", V:64/signed, R/binary>>) -> {long, V, R}; parse_field_value(<<"s", V:16/signed, R/binary>>) -> {short, V, R}; parse_field_value(<<"t", V:8/unsigned, R/binary>>) -> {bool, (V /= 0), R}; parse_field_value(<<"x", VLen:32/unsigned, V:VLen/binary, R/binary>>) -> {binary, V, R}; parse_field_value(<<"V", R/binary>>) -> {void, undefined, R}. ensure_content_decoded(Content = #content{properties = Props}) when Props =/= none -> Content; ensure_content_decoded(Content = #content{properties_bin = PropBin, protocol = Protocol}) when PropBin =/= none -> Content#content{properties = Protocol:decode_properties( Content#content.class_id, PropBin)}. clear_decoded_content(Content = #content{properties = none}) -> Content; clear_decoded_content(Content = #content{properties_bin = none}) -> %% Only clear when we can rebuild the properties later in %% accordance to the content record definition comment - maximum %% one of properties and properties_bin can be 'none' Content; clear_decoded_content(Content = #content{}) -> Content#content{properties = none}. tsung-1.8.0/src/lib/rabbit_binary_generator.erl0000644000201100017670000002532714377756736021325 0ustar nniclausdream%% The contents of this file are subject to the Mozilla Public License %% Version 1.1 (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.mozilla.org/MPL/ %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and %% limitations under the License. %% %% The Original Code is RabbitMQ. %% %% The Initial Developer of the Original Code is VMware, Inc. %% Copyright (c) 2007-2012 VMware, Inc. All rights reserved. %% -module(rabbit_binary_generator). -include("rabbit_framing.hrl"). -include("rabbit.hrl"). -export([build_simple_method_frame/3, build_simple_content_frames/4, build_heartbeat_frame/0]). -export([generate_table/1]). -export([check_empty_frame_size/0]). -export([ensure_content_encoded/2, clear_encoded_content/1]). -export([map_exception/3]). %%---------------------------------------------------------------------------- -ifdef(use_specs). -type(frame() :: [binary()]). -spec(build_simple_method_frame/3 :: (rabbit_channel:channel_number(), rabbit_framing:amqp_method_record(), rabbit_types:protocol()) -> frame()). -spec(build_simple_content_frames/4 :: (rabbit_channel:channel_number(), rabbit_types:content(), non_neg_integer(), rabbit_types:protocol()) -> [frame()]). -spec(build_heartbeat_frame/0 :: () -> frame()). -spec(generate_table/1 :: (rabbit_framing:amqp_table()) -> binary()). -spec(check_empty_frame_size/0 :: () -> 'ok'). -spec(ensure_content_encoded/2 :: (rabbit_types:content(), rabbit_types:protocol()) -> rabbit_types:encoded_content()). -spec(clear_encoded_content/1 :: (rabbit_types:content()) -> rabbit_types:unencoded_content()). -spec(map_exception/3 :: (rabbit_channel:channel_number(), rabbit_types:amqp_error() | any(), rabbit_types:protocol()) -> {rabbit_channel:channel_number(), rabbit_framing:amqp_method_record()}). -endif. %%---------------------------------------------------------------------------- build_simple_method_frame(ChannelInt, MethodRecord, Protocol) -> MethodFields = Protocol:encode_method_fields(MethodRecord), MethodName = rabbit_misc:method_record_type(MethodRecord), {ClassId, MethodId} = Protocol:method_id(MethodName), create_frame(1, ChannelInt, [<>, MethodFields]). build_simple_content_frames(ChannelInt, Content, FrameMax, Protocol) -> #content{class_id = ClassId, properties_bin = ContentPropertiesBin, payload_fragments_rev = PayloadFragmentsRev} = ensure_content_encoded(Content, Protocol), {BodySize, ContentFrames} = build_content_frames(PayloadFragmentsRev, FrameMax, ChannelInt), HeaderFrame = create_frame(2, ChannelInt, [<>, ContentPropertiesBin]), [HeaderFrame | ContentFrames]. build_content_frames(FragsRev, FrameMax, ChannelInt) -> BodyPayloadMax = if FrameMax == 0 -> iolist_size(FragsRev); true -> FrameMax - ?EMPTY_FRAME_SIZE end, build_content_frames(0, [], BodyPayloadMax, [], lists:reverse(FragsRev), BodyPayloadMax, ChannelInt). build_content_frames(SizeAcc, FramesAcc, _FragSizeRem, [], [], _BodyPayloadMax, _ChannelInt) -> {SizeAcc, lists:reverse(FramesAcc)}; build_content_frames(SizeAcc, FramesAcc, FragSizeRem, FragAcc, Frags, BodyPayloadMax, ChannelInt) when FragSizeRem == 0 orelse Frags == [] -> Frame = create_frame(3, ChannelInt, lists:reverse(FragAcc)), FrameSize = BodyPayloadMax - FragSizeRem, build_content_frames(SizeAcc + FrameSize, [Frame | FramesAcc], BodyPayloadMax, [], Frags, BodyPayloadMax, ChannelInt); build_content_frames(SizeAcc, FramesAcc, FragSizeRem, FragAcc, [Frag | Frags], BodyPayloadMax, ChannelInt) -> Size = size(Frag), {NewFragSizeRem, NewFragAcc, NewFrags} = if Size == 0 -> {FragSizeRem, FragAcc, Frags}; Size =< FragSizeRem -> {FragSizeRem - Size, [Frag | FragAcc], Frags}; true -> <> = Frag, {0, [Head | FragAcc], [Tail | Frags]} end, build_content_frames(SizeAcc, FramesAcc, NewFragSizeRem, NewFragAcc, NewFrags, BodyPayloadMax, ChannelInt). build_heartbeat_frame() -> create_frame(?FRAME_HEARTBEAT, 0, <<>>). create_frame(TypeInt, ChannelInt, Payload) -> [<>, Payload, ?FRAME_END]. %% table_field_to_binary supports the AMQP 0-8/0-9 standard types, S, %% I, D, T and F, as well as the QPid extensions b, d, f, l, s, t, x, %% and V. table_field_to_binary({FName, T, V}) -> [short_string_to_binary(FName) | field_value_to_binary(T, V)]. field_value_to_binary(longstr, V) -> ["S", long_string_to_binary(V)]; field_value_to_binary(signedint, V) -> ["I", <>]; field_value_to_binary(decimal, V) -> {Before, After} = V, ["D", Before, <>]; field_value_to_binary(timestamp, V) -> ["T", <>]; field_value_to_binary(table, V) -> ["F", table_to_binary(V)]; field_value_to_binary(array, V) -> ["A", array_to_binary(V)]; field_value_to_binary(byte, V) -> ["b", <>]; field_value_to_binary(double, V) -> ["d", <>]; field_value_to_binary(float, V) -> ["f", <>]; field_value_to_binary(long, V) -> ["l", <>]; field_value_to_binary(short, V) -> ["s", <>]; field_value_to_binary(bool, V) -> ["t", if V -> 1; true -> 0 end]; field_value_to_binary(binary, V) -> ["x", long_string_to_binary(V)]; field_value_to_binary(void, _V) -> ["V"]. table_to_binary(Table) when is_list(Table) -> BinTable = generate_table(Table), [<<(size(BinTable)):32>>, BinTable]. array_to_binary(Array) when is_list(Array) -> BinArray = generate_array(Array), [<<(size(BinArray)):32>>, BinArray]. generate_table(Table) when is_list(Table) -> list_to_binary(lists:map(fun table_field_to_binary/1, Table)). generate_array(Array) when is_list(Array) -> list_to_binary(lists:map(fun ({T, V}) -> field_value_to_binary(T, V) end, Array)). short_string_to_binary(String) when is_binary(String) -> Len = size(String), if Len < 256 -> [<>, String]; true -> exit(content_properties_shortstr_overflow) end; short_string_to_binary(String) -> Len = length(String), if Len < 256 -> [<>, String]; true -> exit(content_properties_shortstr_overflow) end. long_string_to_binary(String) when is_binary(String) -> [<<(size(String)):32>>, String]; long_string_to_binary(String) -> [<<(length(String)):32>>, String]. check_empty_frame_size() -> %% Intended to ensure that EMPTY_FRAME_SIZE is defined correctly. case iolist_size(create_frame(?FRAME_BODY, 0, <<>>)) of ?EMPTY_FRAME_SIZE -> ok; ComputedSize -> exit({incorrect_empty_frame_size, ComputedSize, ?EMPTY_FRAME_SIZE}) end. ensure_content_encoded(Content = #content{properties_bin = PropBin, protocol = Protocol}, Protocol) when PropBin =/= none -> Content; ensure_content_encoded(Content = #content{properties = none, properties_bin = PropBin, protocol = Protocol}, Protocol1) when PropBin =/= none -> Props = Protocol:decode_properties(Content#content.class_id, PropBin), Content#content{properties = Props, properties_bin = Protocol1:encode_properties(Props), protocol = Protocol1}; ensure_content_encoded(Content = #content{properties = Props}, Protocol) when Props =/= none -> Content#content{properties_bin = Protocol:encode_properties(Props), protocol = Protocol}. clear_encoded_content(Content = #content{properties_bin = none, protocol = none}) -> Content; clear_encoded_content(Content = #content{properties = none}) -> %% Only clear when we can rebuild the properties_bin later in %% accordance to the content record definition comment - maximum %% one of properties and properties_bin can be 'none' Content; clear_encoded_content(Content = #content{}) -> Content#content{properties_bin = none, protocol = none}. %% NB: this function is also used by the Erlang client map_exception(Channel, Reason, Protocol) -> {SuggestedClose, ReplyCode, ReplyText, FailedMethod} = lookup_amqp_exception(Reason, Protocol), {ClassId, MethodId} = case FailedMethod of {_, _} -> FailedMethod; none -> {0, 0}; _ -> Protocol:method_id(FailedMethod) end, case SuggestedClose orelse (Channel == 0) of true -> {0, #'connection.close'{reply_code = ReplyCode, reply_text = ReplyText, class_id = ClassId, method_id = MethodId}}; false -> {Channel, #'channel.close'{reply_code = ReplyCode, reply_text = ReplyText, class_id = ClassId, method_id = MethodId}} end. lookup_amqp_exception(#amqp_error{name = Name, explanation = Expl, method = Method}, Protocol) -> {ShouldClose, Code, Text} = Protocol:lookup_amqp_exception(Name), ExplBin = amqp_exception_explanation(Text, Expl), {ShouldClose, Code, ExplBin, Method}; lookup_amqp_exception(Other, Protocol) -> rabbit_log:warning("Non-AMQP exit reason '~p'~n", [Other]), {ShouldClose, Code, Text} = Protocol:lookup_amqp_exception(internal_error), {ShouldClose, Code, Text, none}. amqp_exception_explanation(Text, Expl) -> ExplBin = list_to_binary(Expl), CompleteTextBin = <>, if size(CompleteTextBin) > 255 -> <>; true -> CompleteTextBin end. tsung-1.8.0/src/lib/pgsql_util.erl0000644000201100017670000001754614377756736016637 0ustar nniclausdream%%% File : pgsql_util.erl %%% Author : Christian Sunesson %%% Description : utility functions used in implementation of %%% postgresql driver. %%% Created : 11 May 2005 by Blah -module(pgsql_util). %% Key-Value handling -export([option/2]). %% Networking -export([socket/1]). -export([send/2, send_int/2, send_msg/3]). -export([recv_msg/2, recv_msg/1, recv_byte/2, recv_byte/1]). %% Protocol packing -export([string/1, make_pair/2, split_pair/1]). -export([split_pair_rec/1]). -export([count_string/1, to_string/1]). -export([oids/2, coldescs/2, datacoldescs/3, int16/2]). -export([decode_row/2, decode_descs/1]). -export([errordesc/1]). -export([zip/2]). %% Constructing authentication messages. -export([pass_plain/1, pass_md5/3]). -import(erlang, [md5/1]). -export([hexlist/2]). %% Lookup key in a plist stored in process dictionary under 'options'. %% Default is returned if there is no value for Key in the plist. option(Key, Default) -> Plist = get(options), case proplists:get_value(Key, Plist, Default) of Default -> Default; Value -> Value end. %% Open a TCP connection socket({tcp, Host, Port}) -> gen_tcp:connect(Host, Port, [{active, false}, binary, {packet, raw}], 5000). send(Sock, Packet) -> gen_tcp:send(Sock, Packet). send_int(Sock, Int) -> Packet = <>, gen_tcp:send(Sock, Packet). send_msg(Sock, Code, Packet) when binary(Packet) -> Len = size(Packet) + 4, Msg = <>, gen_tcp:send(Sock, Msg). recv_msg(Sock, Timeout) -> {ok, Head} = gen_tcp:recv(Sock, 5, Timeout), <> = Head, %%io:format("Code: ~p, Size: ~p~n", [Code, Size]), if Size > 4 -> {ok, Packet} = gen_tcp:recv(Sock, Size-4, Timeout), {ok, Code, Packet}; true -> {ok, Code, <<>>} end. recv_msg(Sock) -> recv_msg(Sock, infinity). recv_byte(Sock) -> recv_byte(Sock, infinity). recv_byte(Sock, Timeout) -> case gen_tcp:recv(Sock, 1, Timeout) of {ok, <>} -> {ok, Byte}; E={error, _Reason} -> throw(E) end. %% Convert String to binary string(String) when list(String) -> Bin = list_to_binary(String), <>; string(Bin) when binary(Bin) -> <>. %%% Two zero terminated strings. make_pair(Key, Value) when atom(Key) -> make_pair(atom_to_list(Key), Value); make_pair(Key, Value) when atom(Value) -> make_pair(Key, atom_to_list(Value)); make_pair(Key, Value) when list(Key), list(Value) -> BinKey = list_to_binary(Key), BinValue = list_to_binary(Value), make_pair(BinKey, BinValue); make_pair(Key, Value) when binary(Key), binary(Value) -> <>. split_pair(Bin) when binary(Bin) -> split_pair(binary_to_list(Bin)); split_pair(Str) -> split_pair_rec(Str, norec). split_pair_rec(Bin) when binary(Bin) -> split_pair_rec(binary_to_list(Bin)); split_pair_rec(Arg) -> split_pair_rec(Arg,[]). split_pair_rec([], Acc) -> lists:reverse(Acc); split_pair_rec([0], Acc) -> lists:reverse(Acc); split_pair_rec(S, Acc) -> Fun = fun(C) -> C /= 0 end, {Key, [0|S1]} = lists:splitwith(Fun, S), {Value, [0|Tail]} = lists:splitwith(Fun, S1), case Acc of norec -> {Key, Value}; _ -> split_pair_rec(Tail, [{Key, Value}| Acc]) end. count_string(Bin) when binary(Bin) -> count_string(Bin, 0). count_string(<<>>, N) -> {N, <<>>}; count_string(<<0/integer, Rest/binary>>, N) -> {N, Rest}; count_string(<<_C/integer, Rest/binary>>, N) -> count_string(Rest, N+1). to_string(Bin) when binary(Bin) -> {Count, _} = count_string(Bin, 0), <> = Bin, {binary_to_list(String), Count}. oids(<<>>, Oids) -> lists:reverse(Oids); oids(<>, Oids) -> oids(Rest, [Oid|Oids]). int16(<<>>, Vals) -> lists:reverse(Vals); int16(<>, Vals) -> int16(Rest, [Val|Vals]). coldescs(<<>>, Descs) -> lists:reverse(Descs); coldescs(Bin, Descs) -> {Name, Count} = to_string(Bin), <<_:Count/binary, 0/integer, TableOID:32/integer, ColumnNumber:16/integer, TypeId:32/integer, TypeSize:16/integer-signed, TypeMod:32/integer-signed, FormatCode:16/integer, Rest/binary>> = Bin, Format = case FormatCode of 0 -> text; 1 -> binary end, Desc = {Name, Format, ColumnNumber, TypeId, TypeSize, TypeMod, TableOID}, coldescs(Rest, [Desc|Descs]). datacoldescs(N, <>, Descs) when N >= 0 -> datacoldescs(N-1, Rest, [Data|Descs]); datacoldescs(_N, _, Descs) -> lists:reverse(Descs). decode_descs(Cols) -> decode_descs(Cols, []). decode_descs([], Descs) -> {ok, lists:reverse(Descs)}; decode_descs([Col|ColTail], Descs) -> OidMap = get(oidmap), {Name, Format, ColNumber, Oid, _, _, _} = Col, OidName = dict:fetch(Oid, OidMap), decode_descs(ColTail, [{Name, Format, ColNumber, OidName, [], [], []}|Descs]). decode_row(Types, Values) -> decode_row(Types, Values, []). decode_row([], [], Out) -> {ok, lists:reverse(Out)}; decode_row([Type|TypeTail], [Value|ValueTail], Out0) -> Out1 = decode_col(Type, Value), decode_row(TypeTail, ValueTail, [Out1|Out0]). decode_col({_, text, _, _, _, _, _}, Value) -> binary_to_list(Value); decode_col({_Name, _Format, _ColNumber, varchar, _Size, _Modifier, _TableOID}, Value) -> binary_to_list(Value); decode_col({_Name, _Format, _ColNumber, int4, _Size, _Modifier, _TableOID}, Value) -> <> = Value, Int4; decode_col({_Name, _Format, _ColNumber, Oid, _Size, _Modifier, _TableOID}, Value) -> {Oid, Value}. errordesc(Bin) -> errordesc(Bin, []). errordesc(<<0/integer, _Rest/binary>>, Lines) -> lists:reverse(Lines); errordesc(<>, Lines) -> {String, Count} = to_string(Rest), <<_:Count/binary, 0, Rest1/binary>> = Rest, Msg = case Code of $S -> {severity, list_to_atom(String)}; $C -> {code, String}; $M -> {message, String}; $D -> {detail, String}; $H -> {hint, String}; $P -> {position, list_to_integer(String)}; $p -> {internal_position, list_to_integer(String)}; $W -> {where, String}; $F -> {file, String}; $L -> {line, list_to_integer(String)}; $R -> {routine, String}; Unknown -> {Unknown, String} end, errordesc(Rest1, [Msg|Lines]). %%% Zip two lists together zip(List1, List2) -> zip(List1, List2, []). zip(List1, List2, Result) when List1 =:= []; List2 =:= [] -> lists:reverse(Result); zip([H1|List1], [H2|List2], Result) -> zip(List1, List2, [{H1, H2}|Result]). %%% Authentication utils pass_plain(Password) -> Pass = [Password, 0], list_to_binary(Pass). %% MD5 authentication patch from %% Juhani Rankimies %% (patch slightly rewritten, new bugs are mine :] /Christian Sunesson) %% %% MD5(MD5(password + user) + salt) %% pass_md5(User, Password, Salt) -> Digest = hex(md5([Password, User])), Encrypt = hex(md5([Digest, Salt])), Pass = ["md5", Encrypt, 0], list_to_binary(Pass). hex(B) when binary(B) -> hexlist(binary_to_list(B), []). hexlist([], Acc) -> lists:reverse(Acc); hexlist([N|Rest], Acc) -> HighNibble = (N band 16#f0) bsr 4, LowNibble = (N band 16#0f), hexlist(Rest, [hexdigit(LowNibble), hexdigit(HighNibble)|Acc]). hexdigit(0) -> $0; hexdigit(1) -> $1; hexdigit(2) -> $2; hexdigit(3) -> $3; hexdigit(4) -> $4; hexdigit(5) -> $5; hexdigit(6) -> $6; hexdigit(7) -> $7; hexdigit(8) -> $8; hexdigit(9) -> $9; hexdigit(10) -> $a; hexdigit(11) -> $b; hexdigit(12) -> $c; hexdigit(13) -> $d; hexdigit(14) -> $e; hexdigit(15) -> $f. tsung-1.8.0/src/lib/pgsql_proto.erl0000644000201100017670000005232214377756736017014 0ustar nniclausdream%%% File : pgsql_proto.erl %%% Author : Christian Sunesson %%% Description : PostgreSQL protocol driver %%% Created : 9 May 2005 %%% This is the protocol handling part of the PostgreSQL driver, it turns packages into %%% erlang term messages and back. -module(pgsql_proto). %% TODO: %% When factorizing make clear distinction between message and packet. %% Packet == binary on-wire representation %% Message = parsed Packet as erlang terms. %%% Version 3.0 of the protocol. %%% Supported in postgres from version 7.4 -define(PROTOCOL_MAJOR, 3). -define(PROTOCOL_MINOR, 0). %%% PostgreSQL protocol message codes -define(PG_BACKEND_KEY_DATA, $K). -define(PG_PARAMETER_STATUS, $S). -define(PG_ERROR_MESSAGE, $E). -define(PG_NOTICE_RESPONSE, $N). -define(PG_EMPTY_RESPONSE, $I). -define(PG_ROW_DESCRIPTION, $T). -define(PG_DATA_ROW, $D). -define(PG_READY_FOR_QUERY, $Z). -define(PG_AUTHENTICATE, $R). -define(PG_BIND, $B). -define(PG_PARSE, $P). -define(PG_COMMAND_COMPLETE, $C). -define(PG_PARSE_COMPLETE, $1). -define(PG_BIND_COMPLETE, $2). -define(PG_CLOSE_COMPLETE, $3). -define(PG_PORTAL_SUSPENDED, $s). -define(PG_NO_DATA, $n). -define(PG_COPY_RESPONSE, $G). -export([init/2, idle/2]). -export([run/1]). %% For protocol unwrapping, pgsql_tcp for example. -export([decode_packet/2]). -export([encode_message/2]). -export([encode/2]). -import(pgsql_util, [option/2]). -import(pgsql_util, [socket/1]). -import(pgsql_util, [send/2, send_int/2, send_msg/3]). -import(pgsql_util, [recv_msg/2, recv_msg/1, recv_byte/2, recv_byte/1]). -import(pgsql_util, [string/1, make_pair/2, split_pair/1]). -import(pgsql_util, [count_string/1, to_string/1]). -import(pgsql_util, [coldescs/2, datacoldescs/3]). deliver(Message) -> DriverPid = get(driver), DriverPid ! Message. run(Options) -> Db = spawn_link(pgsql_proto, init, [self(), Options]), {ok, Db}. %% TODO: We should use states instead of process dictionary init(DriverPid, Options) -> put(options, Options), % connection setup options put(driver, DriverPid), % driver's process id %%io:format("Init~n", []), %% Default values: We connect to localhost on the standard TCP/IP %% port. Host = option(host, "localhost"), Port = option(port, 5432), case socket({tcp, Host, Port}) of {ok, Sock} -> connect(Sock); Error -> Reason = {init, Error}, DriverPid ! {pgsql_error, Reason}, exit(Reason) end. connect(Sock) -> %%io:format("Connect~n", []), %% Connection settings for database-login. %% TODO: Check if the default values are relevant: UserName = option(user, "cos"), DatabaseName = option(database, "template1"), %% Make protocol startup packet. Version = <>, User = make_pair(user, UserName), Database = make_pair(database, DatabaseName), StartupPacket = <>, %% Backend will continue with authentication after the startup packet PacketSize = 4 + size(StartupPacket), ok = gen_tcp:send(Sock, <>), authenticate(Sock). authenticate(Sock) -> %% Await authentication request from backend. {ok, Code, Packet} = recv_msg(Sock, 5000), {ok, Value} = decode_packet(Code, Packet), case Value of %% Error response {error_message, Message} -> exit({authentication, Message}); {authenticate, {AuthMethod, Salt}} -> case AuthMethod of 0 -> % Auth ok setup(Sock, []); 1 -> % Kerberos 4 exit({nyi, auth_kerberos4}); 2 -> % Kerberos 5 exit({nyi, auth_kerberos5}); 3 -> % Plaintext password Password = option(password, ""), EncodedPass = encode_message(pass_plain, Password), ok = send(Sock, EncodedPass), authenticate(Sock); 4 -> % Hashed password exit({nyi, auth_crypt}); 5 -> % MD5 password Password = option(password, ""), User = option(user, ""), EncodedPass = encode_message(pass_md5, {User, Password, Salt}), ok = send(Sock, EncodedPass), authenticate(Sock); _ -> exit({authentication, {unknown, AuthMethod}}) end; %% Unknown message received Any -> exit({protocol_error, Any}) end. setup(Sock, Params) -> %% Receive startup messages until ReadyForQuery {ok, Code, Package} = recv_msg(Sock, 5000), {ok, Pair} = decode_packet(Code, Package), case Pair of %% BackendKeyData, necessary for issuing cancel requests {backend_key_data, {Pid, Secret}} -> Params1 = [{secret, {Pid, Secret}}|Params], setup(Sock, Params1); %% ParameterStatus, a key-value pair. {parameter_status, {Key, Value}} -> Params1 = [{{parameter, Key}, Value}|Params], setup(Sock, Params1); %% Error message, with a sequence of <> %% of error descriptions. Code==0 terminates the Reason. {error_message, Message} -> gen_tcp:close(Sock), exit({error_response, Message}); %% Notice Response, with a sequence of <> %% identified fields. Code==0 terminates the Notice. {notice_response, Notice} -> deliver({pgsql_notice, Notice}), setup(Sock, Params); %% Ready for Query, backend is ready for a new query cycle {ready_for_query, Status} -> deliver({pgsql_params, Params}), deliver(pgsql_connected), put(params, Params), connected(Sock); Any -> deliver({unknown_setup, Any}), connected(Sock) end. %% Connected state. Can now start to push messages %% between frontend and backend. But first some setup. connected(Sock) -> DriverPid = get(driver), %% Protocol unwrapping process. Factored out to make future %% SSL and unix domain support easier. Store process under %% 'socket' in the process dictionary. begin Unwrapper = spawn_link(pgsql_tcp, loop0, [Sock, self()]), ok = gen_tcp:controlling_process(Sock, Unwrapper), put(socket, Unwrapper) end, %% Lookup oid to type names and store them in a dictionary under %% 'oidmap' in the process dictionary. begin Packet = encode_message(squery, "SELECT oid, typname FROM pg_type"), ok = send(Sock, Packet), {ok, [{"SELECT", _ColDesc, Rows}]} = process_squery([]), Rows1 = lists:map(fun ([CodeS, NameS]) -> Code = list_to_integer(CodeS), Name = list_to_atom(NameS), {Code, Name} end, Rows), OidMap = dict:from_list(Rows1), put(oidmap, OidMap) end, %% Ready to start marshalling between frontend and backend. idle(Sock, DriverPid). %% In the idle state we should only be receiving requests from the %% frontend. Async backend messages should be forwarded to responsible %% process. idle(Sock, Pid) -> receive %% Unexpected idle messages. No request should continue to the %% idle state before a ready-for-query has been received. {message, Message} -> io:format("Unexpected message when idle: ~p~n", [Message]), idle(Sock, Pid); %% Socket closed or socket error messages. {socket, Sock, Condition} -> exit({socket, Condition}); %% Close connection {terminate, Ref, Pid} -> Packet = encode_message(terminate, []), ok = send(Sock, Packet), gen_tcp:close(Sock), Pid ! {pgsql, Ref, terminated}, unlink(Pid), exit(terminating); %% Simple query {squery, Ref, Pid, Query} -> Packet = encode_message(squery, Query), ok = send(Sock, Packet), {ok, Result} = process_squery([]), case lists:keymember(error, 1, Result) of true -> RBPacket = encode_message(squery, "ROLLBACK"), ok = send(Sock, RBPacket), {ok, RBResult} = process_squery([]); _ -> ok end, Pid ! {pgsql, Ref, Result}, idle(Sock, Pid); %% Extended query %% simplistic version using the unnammed prepared statement and portal. {equery, Ref, Pid, {Query, Params}} -> ParseP = encode_message(parse, {"", Query, []}), BindP = encode_message(bind, {"", "", Params, auto, [binary]}), DescribeP = encode_message(describe, {portal, ""}), ExecuteP = encode_message(execute, {"", 0}), SyncP = encode_message(sync, []), ok = send(Sock, [ParseP, BindP, DescribeP, ExecuteP, SyncP]), {ok, Command, Desc, Status, Logs} = process_equery([]), OidMap = get(oidmap), NameTypes = lists:map(fun({Name, _Format, _ColNo, Oid, _, _, _}) -> {Name, dict:fetch(Oid, OidMap)} end, Desc), Pid ! {pgsql, Ref, {Command, Status, NameTypes, Logs}}, idle(Sock, Pid); %% Prepare a statement, so it can be used for queries later on. {prepare, Ref, Pid, {Name, Query}} -> send_message(Sock, parse, {Name, Query, []}), send_message(Sock, describe, {prepared_statement, Name}), send_message(Sock, sync, []), {ok, State, ParamDesc, ResultDesc} = process_prepare({[], []}), OidMap = get(oidmap), ParamTypes = lists:map(fun (Oid) -> dict:fetch(Oid, OidMap) end, ParamDesc), ResultNameTypes = lists:map(fun ({ColName, _Format, _ColNo, Oid, _, _, _}) -> {ColName, dict:fetch(Oid, OidMap)} end, ResultDesc), Pid ! {pgsql, Ref, {prepared, State, ParamTypes, ResultNameTypes}}, idle(Sock, Pid); %% Close a prepared statement. {unprepare, Ref, Pid, Name} -> send_message(Sock, close, {prepared_statement, Name}), send_message(Sock, sync, []), {ok, _Status} = process_unprepare(), Pid ! {pgsql, Ref, unprepared}, idle(Sock, Pid); %% Execute a prepared statement {execute, Ref, Pid, {Name, Params}} -> %%io:format("execute: ~p ~p ~n", [Name, Params]), begin % Issue first requests for the prepared statement. BindP = encode_message(bind, {"", Name, Params, auto, [binary]}), DescribeP = encode_message(describe, {portal, ""}), ExecuteP = encode_message(execute, {"", 0}), FlushP = encode_message(flush, []), ok = send(Sock, [BindP, DescribeP, ExecuteP, FlushP]) end, receive {pgsql, {bind_complete, _}} -> % Bind reply first. %% Collect response to describe message, %% which gives a hint of the rest of the messages. {ok, Command, Result} = process_execute(Sock, Ref, Pid), begin % Close portal and end extended query. CloseP = encode_message(close, {portal, ""}), SyncP = encode_message(sync, []), ok = send(Sock, [CloseP, SyncP]) end, receive %% Collect response to close message. {pgsql, {close_complete, _}} -> receive %% Collect response to sync message. {pgsql, {ready_for_query, Status}} -> %%io:format("execute: ~p ~p ~p~n", %% [Status, Command, Result]), Pid ! {pgsql, Ref, {Command, Result}}, idle(Sock, Pid); {pgsql, Unknown} -> exit(Unknown) end; {pgsql, Unknown} -> exit(Unknown) end; {pgsql, Unknown} -> exit(Unknown) end; %% More requests to come. %% . %% . %% . Any -> exit({unknown_request, Any}) end. %% In the process_squery state we collect responses until the backend is %% done processing. process_squery(Log) -> receive {pgsql, {row_description, Cols}} -> {ok, Command, Rows} = process_squery_cols([]), process_squery([{Command, Cols, Rows}|Log]); {pgsql, {command_complete, Command}} -> process_squery([Command|Log]); {pgsql, {ready_for_query, Status}} -> {ok, lists:reverse(Log)}; {pgsql, {error_message, Error}} -> process_squery([{error, Error}|Log]); {pgsql, Any} -> process_squery(Log) end. process_squery_cols(Log) -> receive {pgsql, {data_row, Row}} -> process_squery_cols([lists:map(fun binary_to_list/1, Row)|Log]); {pgsql, {command_complete, Command}} -> {ok, Command, lists:reverse(Log)} end. process_equery(Log) -> receive %% Consume parse and bind complete messages when waiting for the first %% first row_description message. What happens if the equery doesn't %% return a result set? {pgsql, {parse_complete, _}} -> process_equery(Log); {pgsql, {bind_complete, _}} -> process_equery(Log); {pgsql, {row_description, Descs}} -> {ok, Descs1} = pgsql_util:decode_descs(Descs), process_equery_datarow(Descs1, Log, {undefined, Descs, undefined}); {pgsql, Any} -> process_equery([Any|Log]) end. process_equery_datarow(Types, Log, Info={Command, Desc, Status}) -> receive %% {pgsql, {command_complete, Command1}} -> process_equery_datarow(Types, Log, {Command1, Desc, Status}); {pgsql, {ready_for_query, Status1}} -> {ok, Command, Desc, Status1, lists:reverse(Log)}; {pgsql, {data_row, Row}} -> {ok, DecodedRow} = pgsql_util:decode_row(Types, Row), process_equery_datarow(Types, [DecodedRow|Log], Info); {pgsql, Any} -> process_equery_datarow(Types, [Any|Log], Info) end. process_prepare(Info={ParamDesc, ResultDesc}) -> receive {pgsql, {no_data, _}} -> process_prepare({ParamDesc, []}); {pgsql, {parse_complete, _}} -> process_prepare(Info); {pgsql, {parameter_description, Oids}} -> process_prepare({Oids, ResultDesc}); {pgsql, {row_description, Desc}} -> process_prepare({ParamDesc, Desc}); {pgsql, {ready_for_query, Status}} -> {ok, Status, ParamDesc, ResultDesc}; {pgsql, Any} -> io:format("process_prepare: ~p~n", [Any]), process_prepare(Info) end. process_unprepare() -> receive {pgsql, {ready_for_query, Status}} -> {ok, Status}; {pgsql, {close_complate, []}} -> process_unprepare(); {pgsql, Any} -> io:format("process_unprepare: ~p~n", [Any]), process_unprepare() end. process_execute(Sock, Ref, Pid) -> %% Either the response begins with a no_data or a row_description %% Needs to return {ok, Status, Result} %% where Result = {Command, ...} receive {pgsql, {no_data, _}} -> {ok, Command, Result} = process_execute_nodata(); {pgsql, {row_description, Descs}} -> {ok, Types} = pgsql_util:decode_descs(Descs), {ok, Command, Result} = process_execute_resultset(Sock, Ref, Pid, Types, []); {pgsql, Unknown} -> exit(Unknown) end. process_execute_nodata() -> receive {pgsql, {command_complete, Command}} -> case Command of "INSERT "++Rest -> {ok, [{integer, _, _Table}, {integer, _, NRows}], _} = erl_scan:string(Rest), {ok, 'INSERT', NRows}; "SELECT" -> {ok, 'SELECT', should_not_happen}; "DELETE "++Rest -> {ok, [{integer, _, NRows}], _} = erl_scan:string(Rest), {ok, 'DELETE', NRows}; Any -> {ok, nyi, Any} end; {pgsql, Unknown} -> exit(Unknown) end. process_execute_resultset(Sock, Ref, Pid, Types, Log) -> receive {pgsql, {command_complete, Command}} -> {ok, list_to_atom(Command), lists:reverse(Log)}; {pgsql, {data_row, Row}} -> {ok, DecodedRow} = pgsql_util:decode_row(Types, Row), process_execute_resultset(Sock, Ref, Pid, Types, [DecodedRow|Log]); {pgsql, {portal_suspended, _}} -> throw(portal_suspended); {pgsql, Any} -> %%process_execute_resultset(Types, [Any|Log]) exit(Any) end. %% With a message type Code and the payload Packet appropriate %% decoding procedure can proceed. decode_packet(Code, Packet) -> Ret = fun(CodeName, Values) -> {ok, {CodeName, Values}} end, case Code of ?PG_ERROR_MESSAGE -> Message = pgsql_util:errordesc(Packet), Ret(error_message, Message); ?PG_EMPTY_RESPONSE -> Ret(empty_response, []); ?PG_ROW_DESCRIPTION -> <> = Packet, Descs = coldescs(ColDescs, []), Ret(row_description, Descs); ?PG_READY_FOR_QUERY -> <> = Packet, case State of $I -> Ret(ready_for_query, idle); $T -> Ret(ready_for_query, transaction); $E -> Ret(ready_for_query, failed_transaction) end; ?PG_COMMAND_COMPLETE -> {Task, _} = to_string(Packet), Ret(command_complete, Task); ?PG_DATA_ROW -> <> = Packet, ColData = datacoldescs(NumberCol, RowData, []), Ret(data_row, ColData); ?PG_BACKEND_KEY_DATA -> <> = Packet, Ret(backend_key_data, {Pid, Secret}); ?PG_PARAMETER_STATUS -> {Key, Value} = split_pair(Packet), Ret(parameter_status, {Key, Value}); ?PG_NOTICE_RESPONSE -> Ret(notice_response, []); ?PG_AUTHENTICATE -> <> = Packet, Ret(authenticate, {AuthMethod, Salt}); ?PG_PARSE_COMPLETE -> Ret(parse_complete, []); ?PG_BIND_COMPLETE -> Ret(bind_complete, []); ?PG_PORTAL_SUSPENDED -> Ret(portal_suspended, []); ?PG_CLOSE_COMPLETE -> Ret(close_complete, []); ?PG_COPY_RESPONSE -> <> = Packet, Format = case FormatCode of 0 -> text; 1 -> binary end, Cols=pgsql_util:int16(ColFormat,[]), Ret(copy_response, {Format,Cols}); $t -> <> = Packet, Oids = pgsql_util:oids(OidsP, []), Ret(parameter_description, Oids); ?PG_NO_DATA -> Ret(no_data, []); Any -> Ret(unknown, [Code]) end. send_message(Sock, Type, Values) -> %%io:format("send_message:~p~n", [{Type, Values}]), Packet = encode_message(Type, Values), ok = send(Sock, Packet). %% Add header to a message. encode(Code, Packet) -> Len = size(Packet) + 4, <>. %% Encode a message of a given type. encode_message(pass_plain, Password) -> Pass = pgsql_util:pass_plain(Password), encode($p, Pass); encode_message(pass_md5, {User, Password, Salt}) -> Pass = pgsql_util:pass_md5(User, Password, Salt), encode($p, Pass); encode_message(terminate, _) -> encode($X, <<>>); encode_message(copydone, _) -> encode($c, <<>>); encode_message(copyfail, Msg) -> encode($f, string(Msg)); encode_message(copy, Data) -> encode($d, Data ); encode_message(squery, Query) -> % squery as in simple query. encode($Q, string(Query)); encode_message(close, {Object, Name}) -> Type = case Object of prepared_statement -> $S; portal -> $P end, String = string(Name), encode($C, <>); encode_message(describe, {Object, Name}) -> ObjectP = case Object of prepared_statement -> $S; portal -> $P end, NameP = string(Name), encode($D, <>); encode_message(flush, _) -> encode($H, <<>>); encode_message(parse, {Name, Query, Oids}) -> StringName = string(Name), StringQuery = string(Query), NOids=length(Oids), OidsBin=lists:foldl(fun(X,Acc)-> << Acc/binary ,X:32/integer>> end, << >>, Oids), encode($P, <>); encode_message(bind, Bind={NamePortal, NamePrepared, Parameters, ParamsFormats,ResultFormats}) -> %%io:format("encode bind: ~p~n", [Bind]), PortalP = string(NamePortal), PreparedP = string(NamePrepared), NParameters = length(Parameters), {NParametersFormat, ParamFormatsP} = case ParamsFormats of none -> {0,<<>>}; binary-> {1,<<1:16/integer>>}; text -> {1,<<0:16/integer>>}; auto -> ParamFormatsList = lists:map( fun (Bin) when is_binary(Bin) -> <<1:16/integer>>; (Text) -> <<0:16/integer>> end, Parameters), {NParameters, erlang:list_to_binary(ParamFormatsList)} end, ParametersList = lists:map( fun (null) -> Minus = -1, <>; (Bin) when is_binary(Bin) -> Size = size(Bin), <>; (Integer) when is_integer(Integer) -> List = integer_to_list(Integer), Bin = list_to_binary(List), Size = size(Bin), <>; (Text) -> Bin = list_to_binary(Text), Size = size(Bin), <> end, Parameters), ParametersP = erlang:list_to_binary(ParametersList), NResultFormats = length(ResultFormats), ResultFormatsList = lists:map( fun (binary) -> <<1:16/integer>>; (text) -> <<0:16/integer>> end, ResultFormats), ResultFormatsP = erlang:list_to_binary(ResultFormatsList), %%io:format("encode bind: ~p~n", [{PortalP, PreparedP, %% NParameters, ParamFormatsP, %% NParameters, ParametersP, %% NResultFormats, ResultFormatsP}]), encode($B, <>); encode_message(execute, {Portal, Limit}) -> String = string(Portal), encode($E, <>); encode_message(sync, _) -> encode($S, <<>>). tsung-1.8.0/src/lib/oauth_uri.erl0000644000201100017670000001050514377756736016437 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth_uri). -export([normalize/1, calate/2, encode/1]). -export([params_from_string/1, params_to_string/1, params_from_header_string/1, params_to_header_string/1]). -import(lists, [concat/1]). -spec normalize(iolist()) -> iolist(). normalize(URI) -> case http_uri:parse(URI) of {ok, {Scheme, UserInfo, Host, Port, Path, _Query}} -> normalize(Scheme, UserInfo, string:to_lower(Host), Port, [Path]); Else -> Else end. normalize(http, UserInfo, Host, 80, Acc) -> normalize(http, UserInfo, [Host|Acc]); normalize(https, UserInfo, Host, 443, Acc) -> normalize(https, UserInfo, [Host|Acc]); normalize(Scheme, UserInfo, Host, Port, Acc) -> normalize(Scheme, UserInfo, [Host, ":", Port|Acc]). normalize(Scheme, [], Acc) -> concat([Scheme, "://"|Acc]); normalize(Scheme, UserInfo, Acc) -> concat([Scheme, "://", UserInfo, "@"|Acc]). -spec params_to_header_string([{string(), string()}]) -> string(). params_to_header_string(Params) -> intercalate(", ", [concat([encode(K), "=\"", encode(V), "\""]) || {K, V} <- Params]). -spec params_from_header_string(string()) -> [{string(), string()}]. params_from_header_string(String) -> [param_from_header_string(Param) || Param <- re:split(String, ",\\s*", [{return, list}])]. param_from_header_string(Param) -> [Key|Rest] = string:tokens(Param, "="), QuotedValue = string:join(Rest,"="), Value = string:substr(QuotedValue, 2, length(QuotedValue) - 2), {decode(Key), decode(Value)}. -spec params_from_string(string()) -> [{string(), string()}]. params_from_string(Params) -> [param_from_string(Param) || Param <- string:tokens(Params, "&")]. param_from_string(Param) -> list_to_tuple([decode(Value) || Value <- string:tokens(Param, "=")]). -spec params_to_string([{string(), string()}]) -> string(). params_to_string(Params) -> intercalate("&", [calate("=", [K, V]) || {K, V} <- Params]). -spec calate(string(), [string()]) -> string(). calate(Sep, Xs) -> intercalate(Sep, [encode(X) || X <- Xs]). intercalate(Sep, Xs) -> concat(intersperse(Sep, Xs)). intersperse(_, []) -> []; intersperse(_, [X]) -> [X]; intersperse(Sep, [X|Xs]) -> [X, Sep|intersperse(Sep, Xs)]. -define(is_alphanum(C), C >= $A, C =< $Z; C >= $a, C =< $z; C >= $0, C =< $9). -spec encode(integer() | atom() | string()) -> string(). encode(Term) when is_integer(Term) -> integer_to_list(Term); encode(Term) when is_atom(Term) -> encode(atom_to_list(Term)); encode(Term) when is_list(Term) -> encode(lists:reverse(Term, []), []). encode([X | T], Acc) when ?is_alphanum(X); X =:= $-; X =:= $_; X =:= $.; X =:= $~ -> encode(T, [X | Acc]); encode([X | T], Acc) -> NewAcc = [$%, dec2hex(X bsr 4), dec2hex(X band 16#0f) | Acc], encode(T, NewAcc); encode([], Acc) -> Acc. decode(Str) when is_list(Str) -> decode(Str, []). decode([$%, A, B | T], Acc) -> decode(T, [(hex2dec(A) bsl 4) + hex2dec(B) | Acc]); decode([X | T], Acc) -> decode(T, [X | Acc]); decode([], Acc) -> lists:reverse(Acc, []). -compile({inline, [{dec2hex, 1}, {hex2dec, 1}]}). dec2hex(N) when N >= 10 andalso N =< 15 -> N + $A - 10; dec2hex(N) when N >= 0 andalso N =< 9 -> N + $0. hex2dec(C) when C >= $A andalso C =< $F -> C - $A + 10; hex2dec(C) when C >= $0 andalso C =< $9 -> C - $0. tsung-1.8.0/src/lib/oauth_unix.erl0000644000201100017670000000266714377756736016635 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth_unix). -export([timestamp/0]). -spec timestamp() -> integer(). timestamp() -> timestamp(calendar:universal_time()). timestamp(DateTime) -> seconds(DateTime) - epoch(). epoch() -> seconds({{1970,1,1},{00,00,00}}). seconds(DateTime) -> calendar:datetime_to_gregorian_seconds(DateTime). tsung-1.8.0/src/lib/oauth_rsa_sha1.erl0000644000201100017670000000444614377756736017350 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth_rsa_sha1). -export([signature/2, verify/3]). -include_lib("public_key/include/public_key.hrl"). -spec signature(string(), string()) -> string(). signature(BaseString, PrivateKeyPath) -> {ok, Contents} = file:read_file(PrivateKeyPath), [Info] = public_key:pem_decode(Contents), PrivateKey = public_key:pem_entry_decode(Info), base64:encode_to_string(public_key:sign(list_to_binary(BaseString), sha, PrivateKey)). -spec verify(string(), string(), term()) -> boolean(). verify(Signature, BaseString, PublicKey) -> public_key:verify(to_binary(BaseString), sha, base64:decode(Signature), public_key(PublicKey)). to_binary(Term) when is_list(Term) -> list_to_binary(Term); to_binary(Term) when is_binary(Term) -> Term. public_key(Path) when is_list(Path) -> {ok, Contents} = file:read_file(Path), [{'Certificate', DerCert, not_encrypted}] = public_key:pem_decode(Contents), public_key( public_key:pkix_decode_cert(DerCert, otp)); public_key(#'OTPCertificate'{tbsCertificate=Cert}) -> public_key(Cert); public_key(#'OTPTBSCertificate'{subjectPublicKeyInfo=Info}) -> public_key(Info); public_key(#'OTPSubjectPublicKeyInfo'{subjectPublicKey=Key}) -> Key. tsung-1.8.0/src/lib/oauth_plaintext.erl0000644000201100017670000000264114377756736017652 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth_plaintext). -export([signature/2, verify/3]). -spec signature(string(), string()) -> string(). signature(CS, TS) -> oauth_uri:calate("&", [CS, TS]). -spec verify(string(), string(), string()) -> boolean(). verify(Signature, CS, TS) -> Signature =:= signature(CS, TS). tsung-1.8.0/src/lib/oauth_http.erl0000644000201100017670000000505114377756736016617 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth_http). -export([get/1, post/2, put/2, response_params/1, response_body/1, response_code/1]). -type http_status() :: {string(), integer(), string()}. -spec get(string()) -> {ok, {Status::http_status(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. get(URL) -> request(get, {URL, []}). -spec post(string(), term()) -> {ok, {Status::http_status(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. post(URL, Data) -> request(post, {URL, [], "application/x-www-form-urlencoded", Data}). -spec put(string(), term()) -> {ok, {Status::http_status(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. put(URL, Data) -> request(put, {URL, [], "application/x-www-form-urlencoded", Data}). -spec request(httpc:method(), tuple()) -> {ok, {Status::http_status(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. request(Method, Request) -> httpc:request(Method, Request, [{autoredirect, false}], []). -spec response_params({http_status(), [{string(), string()}], string()}) -> [{string(), string()}]. response_params(Response) -> oauth_uri:params_from_string(response_body(Response)). -spec response_body({http_status(), [{string(), string()}], string()}) -> string(). response_body({{_, _, _}, _, Body}) -> Body. -spec response_code({http_status(), [{string(), string()}], string()}) -> integer(). response_code({{_, Code, _}, _, _}) -> Code. tsung-1.8.0/src/lib/oauth_hmac_sha1.erl0000644000201100017670000000340214377756736017462 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% Copyright (c) 2015 Christoher Meng %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth_hmac_sha1). -export([signature/3, verify/4]). -spec signature(string(), string(), string()) -> string(). signature(BaseString, CS, TS) -> Key = oauth_uri:calate("&", [CS, TS]), base64:encode_to_string(sha2hmac(Key, BaseString)). sha2hmac(Key, Data) -> case erlang:function_exported(crypto, hmac, 3) of true -> crypto:hmac(sha, Key, Data); false -> crypto:sha_mac(Key, Data) end. -spec verify(string(), string(), string(), string()) -> boolean(). verify(Signature, BaseString, CS, TS) -> Signature =:= signature(BaseString, CS, TS). tsung-1.8.0/src/lib/oauth.erl0000644000201100017670000001344714377756736015570 0ustar nniclausdream%% Copyright (c) 2008-2009 Tim Fletcher %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. -module(oauth). -export( [ get/5 , header/1 , post/5 , put/5 , signature/5 , signature_base_string/3 , signed_params/6 , token/1 , token_secret/1 , uri/2 , verify/6 ]). -spec get(string(), [proplists:property()], oauth_client:consumer(), string(), string()) -> {ok, {Status::tuple(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. get(URL, ExtraParams, Consumer, Token, TokenSecret) -> SignedParams = signed_params("GET", URL, ExtraParams, Consumer, Token, TokenSecret), oauth_http:get(uri(URL, SignedParams)). -spec post(string(), [proplists:property()], oauth_client:consumer(), string(), string()) -> {ok, {Status::tuple(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. post(URL, ExtraParams, Consumer, Token, TokenSecret) -> SignedParams = signed_params("POST", URL, ExtraParams, Consumer, Token, TokenSecret), oauth_http:post(URL, oauth_uri:params_to_string(SignedParams)). -spec put(string(), [proplists:property()], oauth_client:consumer(), string(), string()) -> {ok, {Status::tuple(), Headers::[{string(), string()}], Body::string()}} | {error, term()}. put(URL, ExtraParams, Consumer, Token, TokenSecret) -> SignedParams = signed_params("PUT", URL, ExtraParams, Consumer, Token, TokenSecret), oauth_http:put(URL, oauth_uri:params_to_string(SignedParams)). -spec uri(string(), [proplists:property()]) -> string(). uri(Base, []) -> Base; uri(Base, Params) -> lists:concat([Base, "?", oauth_uri:params_to_string(Params)]). -spec header([{string(), string()}]) -> {string(), string()}. header(Params) -> {"Authorization", "OAuth " ++ oauth_uri:params_to_header_string(Params)}. -spec token([proplists:property()]) -> string(). token(Params) -> proplists:get_value("oauth_token", Params). -spec token_secret([proplists:property()]) -> string(). token_secret(Params) -> proplists:get_value("oauth_token_secret", Params). -spec verify(string(), string(), string(), [proplists:property()], oauth_client:consumer(), string()) -> boolean(). verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) -> case signature_method(Consumer) of plaintext -> oauth_plaintext:verify(Signature, consumer_secret(Consumer), TokenSecret); hmac_sha1 -> BaseString = signature_base_string(HttpMethod, URL, Params), oauth_hmac_sha1:verify(Signature, BaseString, consumer_secret(Consumer), TokenSecret); rsa_sha1 -> BaseString = signature_base_string(HttpMethod, URL, Params), oauth_rsa_sha1:verify(Signature, BaseString, consumer_secret(Consumer)) end. -spec signed_params(string(), string(), [proplists:property()], oauth_client:consumer(), string(), string()) -> [{string(), string()}]. signed_params(HttpMethod, URL, ExtraParams, Consumer, Token, TokenSecret) -> OauthParams = token_param(Token, params(Consumer)), [{"oauth_signature", signature(HttpMethod, URL, OauthParams ++ ExtraParams, Consumer, TokenSecret)} | OauthParams]. -spec signature(string(), string(), [proplists:property()], oauth_client:consumer(), string()) -> string(). signature(HttpMethod, URL, Params, Consumer, TokenSecret) -> case signature_method(Consumer) of plaintext -> oauth_plaintext:signature(consumer_secret(Consumer), TokenSecret); hmac_sha1 -> BaseString = signature_base_string(HttpMethod, URL, Params), oauth_hmac_sha1:signature(BaseString, consumer_secret(Consumer), TokenSecret); rsa_sha1 -> BaseString = signature_base_string(HttpMethod, URL, Params), oauth_rsa_sha1:signature(BaseString, consumer_secret(Consumer)) end. -spec signature_base_string(string(), string(), [proplists:property()]) -> string(). signature_base_string(HttpMethod, URL, Params) -> NormalizedURL = oauth_uri:normalize(URL), NormalizedParams = oauth_uri:params_to_string(lists:sort(Params)), oauth_uri:calate("&", [HttpMethod, NormalizedURL, NormalizedParams]). token_param("", Params) -> Params; token_param(Token, Params) -> [{"oauth_token", Token}|Params]. params(Consumer) -> Nonce = ts_utils:random_alphanumstr(10), % cf. ruby-oauth params(Consumer, oauth_unix:timestamp(), Nonce). params(Consumer, Timestamp, Nonce) -> [ {"oauth_version", "1.0"} , {"oauth_nonce", Nonce} , {"oauth_timestamp", integer_to_list(Timestamp)} , {"oauth_signature_method", signature_method_string(Consumer)} , {"oauth_consumer_key", consumer_key(Consumer)} ]. signature_method_string(Consumer) -> case signature_method(Consumer) of plaintext -> "PLAINTEXT"; hmac_sha1 -> "HMAC-SHA1"; rsa_sha1 -> "RSA-SHA1" end. signature_method(_Consumer={_, _, Method}) -> Method. consumer_secret(_Consumer={_, Secret, _}) -> Secret. consumer_key(_Consumer={Key, _, _}) -> Key. tsung-1.8.0/src/lib/mqtt_frame.erl0000644000201100017670000003372214377756736016605 0ustar nniclausdream%% The MIT License (MIT) %% %% Copyright (c) <2013> %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %% THE SOFTWARE. %% Modified from: https://code.google.com/p/my-mqtt4erl/source/browse/src/mqtt_core.erl. -module(mqtt_frame). -author('hellomatty@gmail.com'). %% %% An erlang client for MQTT (http://www.mqtt.org/) %% -include_lib("mqtt.hrl"). -export([encode/1, decode/1]). -export([set_connect_options/1, set_publish_options/1, command_for_type/1]). %%%=================================================================== %%% API functions %%%=================================================================== encode(#mqtt{} = Message) -> {VariableHeader, Payload} = encode_message(Message), FixedHeader = encode_fixed_header(Message), EncodedLength = encode_length(size(VariableHeader) + size(Payload)), <>. decode(<>) -> case decode_length(Rest) of more -> more; {RemainingLength, Rest1} -> Size = size(Rest1), if Size >= RemainingLength -> <> = Rest1, {decode_message(decode_fixed_header(<>), Body), Left}; true -> more end end; decode(_Data) -> more. set_connect_options(Options) -> set_connect_options(Options, #connect_options{}). set_publish_options(Options) -> set_publish_options(Options, #publish_options{}). command_for_type(Type) -> case Type of ?CONNECT -> connect; ?CONNACK -> connack; ?PUBLISH -> publish; ?PUBACK -> puback; ?PUBREC -> pubrec; ?PUBREL -> pubrel; ?PUBCOMP -> pubcomp; ?SUBSCRIBE -> subscribe; ?SUBACK -> suback; ?UNSUBSCRIBE -> unsubscribe; ?UNSUBACK -> unsuback; ?PINGREQ -> pingreq; ?PINGRESP -> pingresp; ?DISCONNECT -> disconnect; _ -> unknown end. %%%=================================================================== %%% Internal functions %%%=================================================================== set_connect_options([], Options) -> Options; set_connect_options([{keepalive, KeepAlive}|T], Options) -> set_connect_options(T, Options#connect_options{keepalive = KeepAlive}); set_connect_options([{retry, Retry}|T], Options) -> set_connect_options(T, Options#connect_options{retry = Retry}); set_connect_options([{client_id, ClientId}|T], Options) -> set_connect_options(T, Options#connect_options{client_id = ClientId}); set_connect_options([{clean_start, Flag}|T], Options) -> set_connect_options(T, Options#connect_options{clean_start = Flag}); set_connect_options([{connect_timeout, Timeout}|T], Options) -> set_connect_options(T, Options#connect_options{connect_timeout = Timeout}); set_connect_options([{username, UserName}|T], Options) -> set_connect_options(T, Options#connect_options{username = UserName}); set_connect_options([{password, Password}|T], Options) -> set_connect_options(T, Options#connect_options{password = Password}); set_connect_options([#will{} = Will|T], Options) -> set_connect_options(T, Options#connect_options{will = Will}); set_connect_options([UnknownOption|_T], _Options) -> exit({connect, unknown_option, UnknownOption}). set_publish_options([], Options) -> Options; set_publish_options([{qos, QoS}|T], Options) when QoS >= 0, QoS =< 2 -> set_publish_options(T, Options#publish_options{qos = QoS}); set_publish_options([{retain, true}|T], Options) -> set_publish_options(T, Options#publish_options{retain = 1}); set_publish_options([{retain, false}|T], Options) -> set_publish_options(T, Options#publish_options{retain = 0}); set_publish_options([UnknownOption|_T], _Options) -> exit({unknown, publish_option, UnknownOption}). construct_will(WT, WM, WillQoS, WillRetain) -> #will{ topic = WT, message = WM, publish_options = #publish_options{qos = WillQoS, retain = WillRetain} }. decode_message(#mqtt{type = ?CONNECT} = Message, Rest) -> <> = Rest, {VariableHeader, Payload} = split_binary(Rest, 2 + ProtocolNameLength + 4), <<_:16, ProtocolName:ProtocolNameLength/binary, ProtocolVersion:8/big, UsernameFlag:1, PasswordFlag:1, WillRetain:1, WillQoS:2/big, WillFlag:1, CleanStart:1, _:1, KeepAlive:16/big>> = VariableHeader, {ClientId, Will, Username, Password} = case {WillFlag, UsernameFlag, PasswordFlag} of {1, 0, 0} -> [C, WT, WM] = decode_strings(Payload), W = construct_will(WT, WM, WillQoS, WillRetain), {C, W, undefined, undefined}; {1, 1, 0} -> [C, WT, WM, U] = decode_strings(Payload), W = construct_will(WT, WM, WillQoS, WillRetain), {C, W, U, undefined}; {1, 1, 1} -> [C, WT, WM, U, P] = decode_strings(Payload), W = construct_will(WT, WM, WillQoS, WillRetain), {C, W, U, P}; {0, 1, 0} -> [C, U] = decode_strings(Payload), {C, undefined, U, undefined}; {0, 1, 1} -> [C, U, P] = decode_strings(Payload), {C, undefined, U, P}; {0, 0, 0} -> [C] = decode_strings(Payload), {C, undefined, undefined, undefined} end, Message#mqtt{ arg = #connect_options{ client_id = ClientId, protocol_name = binary_to_list(ProtocolName), protocol_version = ProtocolVersion, clean_start = CleanStart =:= 1, will = Will, username = Username, password = Password, keepalive = KeepAlive } }; decode_message(#mqtt{type = ?CONNACK} = Message, Rest) -> <<_:8, ResponseCode:8/big>> = Rest, Message#mqtt{arg = ResponseCode}; decode_message(#mqtt{type = ?PINGRESP} = Message, _Rest) -> Message; decode_message(#mqtt{type = ?PINGREQ} = Message, _Rest) -> Message; decode_message(#mqtt{type = ?PUBLISH, qos = 0} = Message, Rest) -> {<>, _} = split_binary(Rest, 2), {<<_:16, Topic/binary>>, Payload} = split_binary(Rest, 2 + TopicLength), Message#mqtt{ arg = {binary_to_list(Topic), binary_to_list(Payload)} }; decode_message(#mqtt{type = ?PUBLISH} = Message, Rest) -> {<>, _} = split_binary(Rest, 2), {<<_:16, Topic:TopicLength/binary, MessageId:16/big>>, Payload} = split_binary(Rest, 4 + TopicLength), Message#mqtt{ id = MessageId, arg = {binary_to_list(Topic), binary_to_list(Payload)} }; decode_message(#mqtt{type = Type} = Message, Rest) when Type =:= ?PUBACK; Type =:= ?PUBREC; Type =:= ?PUBREL; Type =:= ?PUBCOMP -> <> = Rest, Message#mqtt{ arg = MessageId }; decode_message(#mqtt{type = ?SUBSCRIBE} = Message, Rest) -> {<>, Payload} = split_binary(Rest, 2), Message#mqtt{ id = MessageId, arg = decode_subs(Payload, []) }; decode_message(#mqtt{type = ?SUBACK} = Message, Rest) -> {<>, Payload} = split_binary(Rest, 2), GrantedQoS = lists:map(fun(Item) -> <<_:6, QoS:2/big>> = <>, QoS end, binary_to_list(Payload) ), Message#mqtt{ arg = {MessageId, GrantedQoS} }; decode_message(#mqtt{type = ?UNSUBSCRIBE} = Message, Rest) -> {<>, Payload} = split_binary(Rest, 2), Message#mqtt{ id = MessageId, arg = {MessageId, lists:map(fun(T) -> #sub{topic = T} end, decode_strings(Payload))} }; decode_message(#mqtt{type = ?UNSUBACK} = Message, Rest) -> <> = Rest, Message#mqtt{ arg = MessageId }; decode_message(#mqtt{type = ?DISCONNECT} = Message, _Rest) -> Message; decode_message(Message, Rest) -> exit({decode_message, unexpected_message, {Message, Rest}}). decode_subs(<<>>, Subs) -> lists:reverse(Subs); decode_subs(Bytes, Subs) -> <> = Bytes, <<_:16, Topic:TopicLength/binary, ?UNUSED:6, QoS:2/big, Rest/binary>> = Bytes, decode_subs(Rest, [#sub{topic = binary_to_list(Topic), qos = QoS}|Subs]). encode_message(#mqtt{type = ?CONNACK, arg = ReturnCode}) -> {<>,<<>>}; encode_message(#mqtt{type = ?CONNECT, arg = Options}) -> CleanStart = case Options#connect_options.clean_start of true -> 1; false -> 0 end, {UserNameFlag, UserNameValue} = case Options#connect_options.username of undefined -> {0, undefined}; UserName -> {1, UserName} end, {PasswordFlag, PasswordValue} = case Options#connect_options.password of undefined -> {0, undefined}; Password -> {1, Password} end, {WillFlag, WillQoS, WillRetain, PayloadList} = case Options#connect_options.will of #will{ topic = undefined } -> {0, 0, 0, [encode_string(Options#connect_options.client_id)]}; #will{ topic = "" } -> {0, 0, 0, [encode_string(Options#connect_options.client_id)]}; undefined -> {0, 0, 0, [encode_string(Options#connect_options.client_id)]}; #will{ topic = WillTopic, message = WillMessage, publish_options = WillOptions } -> {1, WillOptions#publish_options.qos, WillOptions#publish_options.retain, [encode_string(Options#connect_options.client_id), encode_string(WillTopic), encode_string(WillMessage)] } end, Payload1 = case UserNameValue of undefined -> list_to_binary(PayloadList); _ -> case PasswordValue of undefined -> list_to_binary(lists:append(PayloadList, [encode_string(UserNameValue)])); _ -> list_to_binary(lists:append(PayloadList, [encode_string(UserNameValue), encode_string(PasswordValue)])) end end, { list_to_binary([ encode_string(Options#connect_options.protocol_name), <<(Options#connect_options.protocol_version)/big>>, <> ]), Payload1 }; encode_message(#mqtt{type = ?PUBLISH, arg = {Topic, Payload}} = Message) -> if Message#mqtt.qos =:= 0 -> { encode_string(Topic), list_to_binary(Payload) }; Message#mqtt.qos > 0 -> { list_to_binary([encode_string(Topic), <<(Message#mqtt.id):16/big>>]), list_to_binary(Payload) } end; encode_message(#mqtt{type = ?PUBACK, arg = MessageId}) -> { <>, <<>> }; encode_message(#mqtt{type = ?SUBSCRIBE, arg = Subs} = Message) -> { <<(Message#mqtt.id):16/big>>, list_to_binary( lists:flatten( lists:map(fun({sub, Topic, RequestedQoS}) -> [encode_string(Topic), <>] end, Subs))) }; encode_message(#mqtt{type = ?SUBACK, arg = {MessageId, Subs}}) -> { <>, list_to_binary(lists:map(fun(S) -> <> end, Subs)) }; encode_message(#mqtt{type = ?UNSUBSCRIBE, arg = Subs} = Message) -> { <<(Message#mqtt.id):16/big>>, list_to_binary(lists:map(fun({sub, T, _Q}) -> encode_string(T) end, Subs)) }; encode_message(#mqtt{type = ?UNSUBACK, arg = MessageId}) -> {<>, <<>>}; encode_message(#mqtt{type = ?PINGREQ}) -> {<<>>, <<>>}; encode_message(#mqtt{type = ?PINGRESP}) -> {<<>>, <<>>}; encode_message(#mqtt{type = ?PUBREC, arg = MessageId}) -> {<>, <<>>}; encode_message(#mqtt{type = ?PUBREL, arg = MessageId}) -> {<>, <<>>}; encode_message(#mqtt{type = ?PUBCOMP, arg = MessageId}) -> {<>, <<>>}; encode_message(#mqtt{type = ?DISCONNECT}) -> {<<>>, <<>>}; encode_message(#mqtt{} = Message) -> exit({encode_message, unknown_type, Message}). decode_length(<<>>) -> more; decode_length(Data) -> decode_length(Data, 1, 0). decode_length(<<>>, Multiplier, Value) -> {0, <<>>}; decode_length(<<0:1, Length:7, Rest/binary>>, Multiplier, Value) -> {Value + Multiplier * Length, Rest}; decode_length(<<1:1, Length:7, Rest/binary>>, Multiplier, Value) -> decode_length(Rest, Multiplier * 128, Value + Multiplier * Length). encode_length(Length) -> encode_length(Length, <<>>). encode_length(Length, Buff) when Length div 128 > 0 -> Digit = Length rem 128, Current = <<1:1, Digit:7/big>>, encode_length(Length div 128, <>); encode_length(Length, Buff) -> Digit = Length rem 128, Current = <<0:1, Digit:7/big>>, <>. encode_fixed_header(Message) when is_record(Message, mqtt) -> <<(Message#mqtt.type):4/big, (Message#mqtt.dup):1, (Message#mqtt.qos):2/big, (Message#mqtt.retain):1>>. decode_fixed_header(Byte) -> <> = Byte, #mqtt{type = Type, dup = Dup, qos = QoS, retain = Retain}. encode_string(String) -> Bytes = list_to_binary(String), Length = size(Bytes), <>. decode_strings(Bytes) when is_binary(Bytes) -> decode_strings(Bytes, []). decode_strings(<<>>, Strings) -> lists:reverse(Strings); decode_strings(<> = Bytes, Strings) -> <<_:16, Binary:Length/binary, Rest/binary>> = Bytes, decode_strings(Rest, [binary_to_list(Binary)|Strings]). tsung-1.8.0/src/lib/mochiweb_xpath_utils.erl0000644000201100017670000000472514377756736020670 0ustar nniclausdream%% xpath_utils.erl %% @author Pablo Polvorin %% @doc Utility functions, mainly for type conversion %% Conversion rules taken from http://www.w3.org/TR/1999/REC-xpath-19991116 %% created on 2008-05-07 -module(mochiweb_xpath_utils). -export([string_value/1, number_value/1, node_set_value/1, boolean_value/1, convert/2]). -spec string_value(mochiweb_xpath:indexed_xpath_return()) -> binary(). string_value(N) when is_list(N)-> case N of [X|_] -> string_value(X); [] -> <<>> end; string_value({_,_,Contents,_}) -> %% Node L = lists:filter(fun ({_,_,_,_}) ->false; (B) when is_binary(B) -> true end,Contents), list_to_binary(L); string_value({_Name, Value}) -> %% attribute Value; string_value(N) when is_integer(N) -> list_to_binary(integer_to_list(N)); string_value(B) when is_binary(B) -> B; string_value(B) when is_atom(B) -> list_to_binary(atom_to_list(B)); string_value(Expr) -> %% string_value(mochiweb_xpath:execute_expr(Expr, Ctx)). throw({not_implemented, "String from expression", Expr}). -spec node_set_value(mochiweb_xpath:indexed_xpath_return()) -> [mochiweb_xpath:indexed_html_node()]. node_set_value(List) when is_list(List) -> List; node_set_value(N) -> throw({node_set_expected,N}). -spec number_value(mochiweb_xpath:indexed_xpath_return() | binary()) -> number(). number_value(N) when is_integer(N) or is_float(N) -> N; number_value({number, N}) when is_integer(N) or is_float(N) -> N; number_value({negative, Exp}) -> N = number_value(Exp), - N; number_value(N) when is_binary(N)-> String = binary_to_list(N), case erl_scan:string(String) of {ok, [{integer,1,I}],1} -> I; {ok, [{float,1,F}],1} -> F end; number_value(N) -> number_value(string_value(N)). -spec boolean_value(mochiweb_xpath:indexed_xpath_return()) -> boolean(). boolean_value([]) -> false; boolean_value([_|_]) -> true; boolean_value(N) when is_number(N) -> N /= 0; boolean_value(B) when is_binary(B) -> size(B) /= 0; boolean_value(B) when is_boolean(B) -> B; boolean_value({_, _, _Contents, _}) -> true; % TODO: rly? boolean_value(_Expr) -> throw({not_implemented, "Boolean from expression"}). convert(Value,number) -> number_value(Value); convert(Value,string) -> string_value(Value); convert(Value,node_set) -> node_set_value(Value); convert(Value, boolean) -> boolean_value(Value). tsung-1.8.0/src/lib/mochiweb_xpath_parser.erl0000644000201100017670000000445314377756736021022 0ustar nniclausdream%% @author Pablo Polvorin %% @doc Compile XPath expressions. %% This module uses the xpath parser of xmerl.. that interface isn't documented %% so could change between OTP versions.. its know to work with OTP R12B2 %% created on 2008-05-07 -module(mochiweb_xpath_parser). -export([compile_xpath/1]). %% Return a compiled XPath expression compile_xpath(XPathString) -> {ok,XPath} = xmerl_xpath_parse:parse(xmerl_xpath_scan:tokens(XPathString)), simplify(XPath). %% @doc Utility functions to convert between the *internal* representation of % xpath expressions used in xmerl(using lists and atoms), to a % representation using only binaries, to match the way in % which the mochiweb html parser represents data simplify({path, union, {Path1, Path2}})-> %% "expr1 | expr2 | expr3" {path, union, {simplify(Path1), simplify(Path2)}}; simplify({path,Type,Path}) -> {path,Type,simplify_path(Path)}; simplify({comp,Comp,A,B}) -> {comp,Comp,simplify(A),simplify(B)}; simplify({literal,L}) -> {literal,list_to_binary(L)}; simplify({number,N}) -> {number,N}; simplify({negative, Smth}) -> {negative, simplify(Smth)}; simplify({bool, Comp, A, B}) -> {bool, Comp, simplify(A), simplify(B)}; simplify({function_call,Fun,Args}) -> {function_call,Fun,lists:map(fun simplify/1,Args)}; simplify({arith, Op, Arg1, Arg2}) -> {arith, Op, simplify(Arg1), simplify(Arg2)}. simplify_path({step,{Axis,NodeTest,Predicates}}) -> {step,{Axis, simplify_node_test(NodeTest), simplify_predicates(Predicates)}}; simplify_path({refine,Path1,Path2}) -> {refine,simplify_path(Path1),simplify_path(Path2)}. simplify_node_test({name,{Tag,Prefix,Local}}) -> {name,{to_binary(Tag),Prefix,Local}}; simplify_node_test(A={node_type, _Type}) -> A; simplify_node_test({processing_instruction, Name}) -> {processing_instruction, list_to_binary(Name)}; % strictly, this must be node_type too! simplify_node_test(A={wildcard,wildcard}) -> A; simplify_node_test({prefix_test, Prefix}) -> %% [37] /prefix:*/ - namespace test {prefix_test, list_to_binary(Prefix)}. simplify_predicates(X) -> lists:map(fun simplify_predicate/1,X). simplify_predicate({pred,Pred}) -> {pred,simplify(Pred)}. to_binary(X) when is_atom(X) -> list_to_binary(atom_to_list(X)). tsung-1.8.0/src/lib/mochiweb_xpath_functions.erl0000644000201100017670000001211114377756736021524 0ustar nniclausdream%% xpath_functions.erl %% @author Pablo Polvorin %% @doc Some core xpath functions that can be used in xpath expressions %% created on 2008-05-07 -module(mochiweb_xpath_functions). -export([lookup_function/1]). %% Default functions. %% The format is: {FunctionName, fun(), FunctionSignature} %% WildCard argspec must be the last spec in list. %% %% @type FunctionName = atom() %% @type FunctionSignature = [XPathArgSpec] %% @type XPathArgSpec = XPathType | WildCardArgSpec %% @type WildCardArgSpec = {'*', XPathType} %% @type XPathType = node_set | string | number | boolean %% %% The engine is responsible of calling the function with %% the correct arguments, given the function signature. -spec lookup_function(atom()) -> mochiweb_xpath:xpath_fun_spec() | false. lookup_function('last') -> {'last',fun last/2,[]}; lookup_function('position') -> {'position',fun position/2,[]}; lookup_function('count') -> {'count',fun count/2,[node_set]}; lookup_function('concat') -> {'concat',fun concat/2,[{'*', string}]}; lookup_function('name') -> {'name',fun 'name'/2,[node_set]}; lookup_function('starts-with') -> {'starts-with', fun 'starts-with'/2,[string,string]}; lookup_function('contains') -> {'contains', fun 'contains'/2,[string,string]}; lookup_function('substring') -> {'substring', fun substring/2,[string,number,number]}; lookup_function('sum') -> {'sum', fun sum/2,[node_set]}; lookup_function('string-length') -> {'string-length', fun 'string-length'/2,[string]}; lookup_function('not') -> {'not', fun x_not/2, [boolean]}; lookup_function('string') -> {'string', fun 'string'/2, [node_set]}; lookup_function(_) -> false. %% @doc Function: boolean last() %% The position function returns the position of the current node last({ctx, _, _, _, Position, Size} = _Ctx, []) -> Position =:= Size. %% @doc Function: number position() %% The position function returns the position of the current node position({ctx, _, _, _, Position, _} = _Ctx, []) -> Position. %% @doc Function: number count(node-set) %% The count function returns the number of nodes in the %% argument node-set. count(_Ctx,[NodeList]) -> length(NodeList). %% @doc Function: concat(binary, binary, ...) %% Concatenate string arguments (variable length) concat(_Ctx, BinariesList) -> %% list_to_binary() << <> || Str <- BinariesList>>. %% @doc Function: string name(node-set?) 'name'(_Ctx,[[{Tag,_,_,_}|_]]) -> Tag. %% @doc Function: boolean starts-with(string, string) %% The starts-with function returns true if the first argument string %% starts with the second argument string, and otherwise returns false. 'starts-with'(_Ctx,[Left,Right]) -> Size = size(Right), case Left of <> -> true; _ -> false end. %% @doc Function: checks that Where contains What contains(_Ctx,[Where, What]) -> case binary:match(Where, [What]) of nomatch -> false; {_, _} -> true end. %% @doc Function: string substring(string, number, number?) %% The substring function returns the substring of the first argument %% starting at the position specified in the second argument with length %% specified in the third argument substring(_Ctx,[String,Start,Length]) when is_binary(String)-> Before = Start -1, After = size(String) - Before - Length, case (Start + Length) =< size(String) of true -> <<_:Before/binary,R:Length/binary,_:After/binary>> = String, R; false -> <<>> end. %% @doc Function: number sum(node-set) %% The sum function returns the sum, for each node in the argument %% node-set, of the result of converting the string-values of the node %% to a number. sum(_Ctx,[Values]) -> lists:sum([mochiweb_xpath_utils:number_value(V) || V <- Values]). %% @doc Function: number string-length(string?) %% The string-length returns the number of characters in the string %% TODO: this isn't true: currently it returns the number of bytes %% in the string, that isn't the same 'string-length'(_Ctx,[String]) -> size(String). %% @doc Function: string string(node_set) %% %% The sum function returns the string representation of the %% nodes in a node-set. This is different from text() in that %% it concatenates each bit of the text in the node along with the text in %% any children nodes along the way, in order. %% Note: this differs from normal xpath in that it returns a list of strings, one %% for each node in the node set, as opposed to just the first node. 'string'(_Ctx, [NodeList]) -> lists:map(fun({_Elem, _Attr, Children,_Pos}) -> concat_child_text(Children, []) end, NodeList). concat_child_text([], Result) -> list_to_binary(lists:reverse(Result)); concat_child_text([{_,_,Children,_} | Rest], Result) -> concat_child_text(Rest, [concat_child_text(Children, []) | Result]); concat_child_text([X | Rest], Result) -> concat_child_text(Rest, [X | Result]). x_not(_Ctx, [Bool]) -> not Bool. tsung-1.8.0/src/lib/mochiweb_xpath.erl0000644000201100017670000004511014377756736017441 0ustar nniclausdream%% mochiweb_html_xpath.erl %% @author Pablo Polvorin %% created on <2008-04-29> %% %% XPath interpreter, navigate mochiweb's html structs %% Only a subset of xpath is implemented, see what is supported in test.erl -module(mochiweb_xpath). -export([execute/2,execute/3,compile_xpath/1]). -export_type([xpath_return/0, html_node/0]). -export_type([xpath_fun_spec/0, xpath_fun/0, xpath_func_argspec/0, xpath_func_context/0]). -export_type([indexed_xpath_return/0, indexed_html_node/0]). % internal!!! %internal data -record(ctx, { root :: indexed_html_node(), ctx :: [indexed_html_node()], functions :: [xpath_fun_spec()], position :: integer(), size :: integer() }). %% HTML tree specs -type html_comment() :: {comment, binary()}. %% -type html_doctype() :: {doctype, [binary()]}. -type html_pi() :: {pi, binary(), binary()} | {pi, binary()}. -type html_attr() :: {binary(), binary()}. -type html_node() :: {binary(), [html_attr()], [html_node() | binary() | html_comment() | html_pi()]}. -type indexed_html_node() :: {binary(), [html_attr()], [indexed_html_node() | binary() | html_comment() | html_pi()], [non_neg_integer()]}. %% XPath return results specs -type xpath_return_item() :: boolean() | number() | binary() | html_node(). -type xpath_return() :: boolean() | number() | [xpath_return_item()]. -type indexed_return_item() :: boolean() | number() | binary() | indexed_html_node(). -type indexed_xpath_return() :: boolean() | number() | [indexed_return_item()]. -type compiled_xpath() :: tuple(). %% XPath functions specs -type xpath_func_context() :: #ctx{}. -type xpath_type() :: node_set | string | number | boolean. -type xpath_func_argspec() :: [xpath_type() | {'*', xpath_type()}]. -type xpath_fun() :: fun((FuncCtx :: xpath_func_context(), FuncArgs :: indexed_xpath_return()) -> FuncReturn :: indexed_xpath_return()). -type xpath_fun_spec() :: {atom(), xpath_fun(), xpath_func_argspec()}. %% %% API %% -spec compile_xpath( string() ) -> compiled_xpath(). compile_xpath(Expr) -> mochiweb_xpath_parser:compile_xpath(Expr). %% @doc Execute the given XPath expression against the given document, using %% the default set of functions. %% @spec execute(XPath,Doc) -> Results %% @type XPath = compiled_xpath() | string() %% @type Doc = node() %% @type Results = [node()] | binary() | boolean() | number() -spec execute(XPath, Doc) -> Results when XPath :: compiled_xpath() | string(), Doc :: html_node(), Results :: xpath_return(). execute(XPath,Root) -> execute(XPath,Root,[]). %% @doc Execute the given XPath expression against the given document, %% using the default set of functions plus the user-supplied ones. %% %% @see mochiweb_xpath_functions.erl to see how to write functions %% %% @spec execute(XPath,Doc,Functions) -> Results %% @type XPath = compiled_xpath() | string() %% @type Doc = node() %% @type Functions = [FunctionDefinition] %% @type FunctionDefinition = {FunName,Fun,Signature} %% @type FunName = atom() %% @type Fun = fun/2 %% @type Signature = [ArgType] %% @type ArgType = node_set | string | number | boolean %% @type Results = [node()] | binary() | boolean() | number() %% TODO: should pass the user-defined functions when compiling %% the xpath expression (compile_xpath/1). Then the %% compiled expression would have all its functions %% resolved, and no function lookup would occur when %% the expression is executed -spec execute(XPath, Doc, Functions) -> Result when XPath :: string() | compiled_xpath(), Doc :: html_node(), Functions :: [xpath_fun_spec()], Result :: xpath_return(). execute(XPathString,Doc,Functions) when is_list(XPathString) -> XPath = mochiweb_xpath_parser:compile_xpath(XPathString), execute(XPath,Doc,Functions); execute(XPath,Doc,Functions) -> R0 = {<<0>>,[],[Doc]}, %% TODO: set parent instead of positions list, or some lazy-positioning? R1 = add_positions(R0), Result = execute_expr(XPath,#ctx{ctx=[R1], root=R1, functions=Functions, position=0}), remove_positions(Result). %% %% XPath tree traversing, top-level XPath interpreter %% %% xmerl_xpath:match_expr/2 execute_expr({path, Type, Arg}, S) -> eval_path(Type, Arg, S); execute_expr(PrimExpr, S) -> eval_primary_expr(PrimExpr, S). eval_path(union, {PathExpr1, PathExpr2}, C) -> %% in XPath 1.0 union doesn't necessary must return nodes in document %% order (but must in XPath 2.0) S1 = execute_expr(PathExpr1, C), S2 = execute_expr(PathExpr2, C), ordsets:to_list(ordsets:union(ordsets:from_list(S1), ordsets:from_list(S2))); eval_path(abs, Path ,Ctx = #ctx{root=Root}) -> do_path_expr(Path, Ctx#ctx{ctx=[Root]}); eval_path(rel, Path, Ctx) -> do_path_expr(Path, Ctx); eval_path(filter, {_PathExpr, {pred, _Pred}}, _C) -> throw({not_implemented, "filter"}). % Who needs them? eval_primary_expr({comp,Comp,A,B},Ctx) -> %% for predicates CompFun = comp_fun(Comp), L = execute_expr(A,Ctx), R = execute_expr(B,Ctx), comp(CompFun,L,R); eval_primary_expr({arith, Op, Arg1, Arg2}, Ctx) -> %% for predicates L = execute_expr(Arg1,Ctx), R = execute_expr(Arg2,Ctx), arith(Op, L, R); eval_primary_expr({bool,Comp,A,B},Ctx) -> CompFun = bool_fun(Comp), L = execute_expr(A,Ctx), R = execute_expr(B,Ctx), comp(CompFun,L,R); eval_primary_expr({literal,L},_Ctx) -> [L]; eval_primary_expr({number,N},_Ctx) -> [N]; eval_primary_expr({negative, A}, Ctx) -> R = execute_expr(A, Ctx), [-mochiweb_xpath_utils:number_value(R)]; eval_primary_expr({function_call, Fun, Args}, Ctx=#ctx{functions=Funs}) -> %% TODO: refactor double-case case mochiweb_xpath_functions:lookup_function(Fun) of {Fun, F, FormalSignature} -> call_xpath_function(F, Args, FormalSignature, Ctx); false -> case lists:keysearch(Fun,1,Funs) of {value, {Fun, F, FormalSignature}} -> call_xpath_function(F, Args, FormalSignature, Ctx); false -> throw({efun_not_found, Fun}) end end. call_xpath_function(F, Args, FormalSignature, Ctx) -> TypedArgs = prepare_xpath_function_args(Args, FormalSignature, Ctx), F(Ctx, TypedArgs). %% execute function args expressions and convert them using formal %% signatures prepare_xpath_function_args(Args, Specs, Ctx) -> RealArgs = [execute_expr(Arg, Ctx) || Arg <- Args], convert_xpath_function_args(RealArgs, Specs, []). convert_xpath_function_args([], [], Acc) -> lists:reverse(Acc); convert_xpath_function_args(Args, [{'*', Spec}], Acc) -> NewArgs = [mochiweb_xpath_utils:convert(Arg,Spec) || Arg <- Args], lists:reverse(Acc) ++ NewArgs; convert_xpath_function_args([Arg | Args], [Spec | Specs], Acc) -> NewAcc = [mochiweb_xpath_utils:convert(Arg,Spec) | Acc], convert_xpath_function_args(Args, Specs, NewAcc). do_path_expr({step,{Axis,NodeTest,Predicates}}=_S,Ctx=#ctx{}) -> NewNodeList = axis(Axis, NodeTest, Ctx), apply_predicates(Predicates,NewNodeList,Ctx); do_path_expr({refine,Step1,Step2},Ctx) -> S1 = do_path_expr(Step1,Ctx), do_path_expr(Step2,Ctx#ctx{ctx=S1}). %% %% Axes %% %% TODO: port all axes to use test_node/3 axis('self', NodeTest, #ctx{ctx=Context}) -> [N || N <- Context, test_node(NodeTest, N, Context)]; axis('descendant', NodeTest, #ctx{ctx=Context}) -> [N || {_,_,Children,_} <- Context, N <- descendant_or_self(Children, NodeTest, [], Context)]; axis('descendant_or_self', NodeTest, #ctx{ctx=Context}) -> descendant_or_self(Context, NodeTest, [], Context); axis('child', NodeTest, #ctx{ctx=Context}) -> %% Flat list of all child nodes of Context that pass NodeTest [N || {_,_,Children,_} <- Context, N <- Children, test_node(NodeTest, N, Context)]; axis('parent', NodeTest, #ctx{root=Root, ctx=Context}) -> L = lists:foldl( fun({_,_,_,Position}, Acc) -> ParentPosition = get_parent_position(Position), ParentNode = get_node_at(Root, ParentPosition), maybe_add_node(ParentNode, NodeTest, Acc, Context); (Smth, _Acc) -> throw({not_implemented, "parent for non-nodes", Smth}) end, [], Context), ordsets:to_list(ordsets:from_list(lists:reverse(L))); axis('ancestor', _Test, _Ctx) -> throw({not_implemented, "ancestor axis"}); axis('following_sibling', NodeTest, #ctx{root=Root, ctx=Context}) -> %% TODO: alerts for non-elements (like for `text()/parent::`) [N || {_,_,_,Position} <- Context, N <- begin ParentPosition = get_parent_position(Position), MyPosition = get_position_in_parent(Position), {_,_,Children,_} = get_node_at(Root, ParentPosition), lists:sublist(Children, MyPosition + 1, length(Children) - MyPosition) end, test_node(NodeTest, N, Context)]; axis('preceding_sibling', NodeTest, #ctx{root=Root, ctx=Context}) -> %% TODO: alerts for non-elements (like for `text()/parent::`) [N || {_,_,_,Position} <- Context, N <- begin ParentPosition = get_parent_position(Position), MyPosition = get_position_in_parent(Position), {_,_,Children,_} = get_node_at(Root, ParentPosition), lists:sublist(Children, MyPosition - 1) end, test_node(NodeTest, N, Context)]; axis('following', _Test, _Ctx) -> throw({not_implemented, "following axis"}); axis('preceeding', _Test, _Ctx) -> throw({not_implemented, "preceeding axis"}); axis('attribute', NodeTest, #ctx{ctx=Context}) -> %% Flat list of *attribute values* of Context, that pass NodeTest %% TODO: maybe return attribute {Name, Value} will be better then %% value only? [Value || {_,Attributes,_,_} <- Context, {_Name, Value} = A <- Attributes, test_node(NodeTest, A, Context)]; axis('namespace', _Test, _Ctx) -> throw({not_implemented, "namespace axis"}); axis('ancestor_or_self', _Test, _Ctx) -> throw({not_implemented, "ancestor-or-self axis"}). descendant_or_self(Nodes, NodeTest, Acc, Ctx) -> lists:reverse(do_descendant_or_self(Nodes, NodeTest, Acc, Ctx)). do_descendant_or_self([], _, Acc, _) -> Acc; do_descendant_or_self([Node = {_, _, Children, _} | Rest], NodeTest, Acc, Ctx) -> %% depth-first (document order) NewAcc1 = maybe_add_node(Node, NodeTest, Acc, Ctx), NewAcc2 = do_descendant_or_self(Children, NodeTest, NewAcc1, Ctx), do_descendant_or_self(Rest, NodeTest, NewAcc2, Ctx); do_descendant_or_self([_Smth | Rest], NodeTest, Acc, Ctx) -> %% NewAcc = maybe_add_node(Smth, NodeTest, Acc, Ctx), - no attribs or texts do_descendant_or_self(Rest, NodeTest, Acc, Ctx). %% Except text nodes test_node({wildcard, wildcard}, Element, _Ctx) when not is_binary(Element) -> true; test_node({prefix_test, Prefix}, {Tag, _, _, _}, _Ctx) -> test_ns_prefix(Tag, Prefix); test_node({prefix_test, Prefix}, {AttrName, _}, _Ctx) -> test_ns_prefix(AttrName, Prefix); test_node({name, {Tag, _, _}}, {Tag, _, _, _}, _Ctx) -> true; test_node({name, {AttrName, _, _}}, {AttrName, _}, _Ctx) -> %% XXX: check this! true; test_node({node_type, text}, Text, _Ctx) when is_binary(Text) -> true; test_node({node_type, node}, {_, _, _, _}, _Ctx) -> true; test_node({node_type, node}, Text, _Ctx) when is_binary(Text) -> true; test_node({node_type, node}, {_, _}, _Ctx) -> true; %% test_node({node_type, attribute}, {_, _}, _Ctx) -> %% true; [38] - attribute() not exists! test_node({node_type, comment}, {comment, _}, _Ctx) -> true; test_node({node_type, processing_instruction}, {pi, _}, _Ctx) -> true; test_node({processing_instruction, Name}, {pi, Node}, _Ctx) -> NSize = size(Name), case Node of <> -> true; _ -> false end; test_node(_Other, _N, _Ctx) -> false. test_ns_prefix(Name, Prefix) -> PSize = size(Prefix), case Name of <> -> true; _ -> false end. %% Append Node to Acc only when NodeTest passed maybe_add_node(Node, NodeTest, Acc, Ctx) -> case test_node(NodeTest, Node, Ctx) of true -> [Node | Acc]; false -> Acc end. %% used for predicate indexing %% is_reverse_axis(ancestor) -> %% true; %% is_reverse_axis(ancestor_or_self) -> %% true; %% is_reverse_axis(preceding) -> %% true; %% is_reverse_axis(preceding_sibling) -> %% true; %% is_reverse_axis(_) -> %% flase. %% %% Predicates %% apply_predicates(Predicates,NodeList,Ctx) -> lists:foldl(fun({pred, Pred} ,Nodes) -> apply_predicate(Pred,Nodes,Ctx) end, NodeList,Predicates). % special case: indexing apply_predicate({number,N}, NodeList, _Ctx) when length(NodeList) >= N -> [lists:nth(N,NodeList)]; apply_predicate(Pred, NodeList,Ctx) -> Size = length(NodeList), Filter = fun(Node, {AccPosition, AccNodes0}) -> Predicate = mochiweb_xpath_utils:boolean_value( execute_expr(Pred,Ctx#ctx{ctx=[Node], position=AccPosition, size = Size})), AccNodes1 = if Predicate -> [Node|AccNodes0]; true -> AccNodes0 end, {AccPosition+1, AccNodes1} end, {_, L} = lists:foldl(Filter,{1,[]},NodeList), lists:reverse(L). %% %% Compare functions %% %% @see http://www.w3.org/TR/1999/REC-xpath-19991116 , section 3.4 comp(CompFun,L,R) when is_list(L), is_list(R) -> lists:any(fun(LeftValue) -> lists:any(fun(RightValue)-> CompFun(LeftValue,RightValue) end, R) end, L); comp(CompFun,L,R) when is_list(L) -> lists:any(fun(LeftValue) -> CompFun(LeftValue,R) end,L); comp(CompFun,L,R) when is_list(R) -> lists:any(fun(RightValue) -> CompFun(L,RightValue) end,R); comp(CompFun,L,R) -> CompFun(L,R). -spec comp_fun(atom()) -> fun((indexed_xpath_return(), indexed_xpath_return()) -> boolean()). comp_fun('=') -> fun (A,B) when is_number(A) -> A == mochiweb_xpath_utils:number_value(B); (A,B) when is_number(B) -> mochiweb_xpath_utils:number_value(A) == B; (A,B) when is_boolean(A) -> A == mochiweb_xpath_utils:boolean_value(B); (A,B) when is_boolean(B) -> mochiweb_xpath_utils:boolean_value(A) == B; (A,B) -> mochiweb_xpath_utils:string_value(A) == mochiweb_xpath_utils:string_value(B) end; comp_fun('!=') -> fun(A,B) -> F = comp_fun('='), not F(A,B) end; comp_fun('>') -> fun(A,B) -> mochiweb_xpath_utils:number_value(A) > mochiweb_xpath_utils:number_value(B) end; comp_fun('<') -> fun(A,B) -> mochiweb_xpath_utils:number_value(A) < mochiweb_xpath_utils:number_value(B) end; comp_fun('<=') -> fun(A,B) -> mochiweb_xpath_utils:number_value(A) =< mochiweb_xpath_utils:number_value(B) end; comp_fun('>=') -> fun(A,B) -> mochiweb_xpath_utils:number_value(A) >= mochiweb_xpath_utils:number_value(B) end. %% %% Boolean functions %% bool_fun('and') -> fun(A, B) -> mochiweb_xpath_utils:boolean_value(A) andalso mochiweb_xpath_utils:boolean_value(B) end; bool_fun('or') -> fun(A, B) -> mochiweb_xpath_utils:boolean_value(A) orelse mochiweb_xpath_utils:boolean_value(B) end. %% TODO more boolean operators %% %% Arithmetic functions %% -spec arith(atom(), indexed_xpath_return(), indexed_xpath_return()) -> number(). arith('+', Arg1, Arg2) -> mochiweb_xpath_utils:number_value(Arg1) + mochiweb_xpath_utils:number_value(Arg2); arith('-', Arg1, Arg2) -> mochiweb_xpath_utils:number_value(Arg1) - mochiweb_xpath_utils:number_value(Arg2); arith('*', Arg1, Arg2) -> mochiweb_xpath_utils:number_value(Arg1) * mochiweb_xpath_utils:number_value(Arg2); arith('div', Arg1, Arg2) -> mochiweb_xpath_utils:number_value(Arg1) / mochiweb_xpath_utils:number_value(Arg2); arith('mod', Arg1, Arg2) -> mochiweb_xpath_utils:number_value(Arg1) rem mochiweb_xpath_utils:number_value(Arg2). %% %% Helpers %% %% @doc Add a position to each node %% @spec add_positions(Doc) -> ExtendedDoc %% @type ExtendedDoc = {atom(), [{binary(), any()}], [extended_node()], [non_neg_integer()]} -spec add_positions(html_node()) -> indexed_html_node(). add_positions(Node) -> R = add_positions_aux(Node, []), R. add_positions_aux({Tag,Attrs,Children}, Position) -> {_, NewChildren} = lists:foldl(fun(Child, {Count, AccChildren}) -> NewChild = add_positions_aux(Child, [Count | Position]), {Count+1, [NewChild|AccChildren]} end, {1, []}, Children), {Tag, Attrs, lists:reverse(NewChildren), Position}; add_positions_aux(Data, _) -> Data. %% @doc Remove position from each node %% @spec remove_positions(ExtendedDoc) -> Doc %% @type ExtendedDoc = {atom(), [{binary(), any()}], [extended_node()], [non_neg_integer()]} -spec remove_positions(indexed_xpath_return()) -> xpath_return(). remove_positions(Nodes) when is_list(Nodes) -> [ remove_positions(SubNode) || SubNode <- Nodes ]; remove_positions({Tag, Attrs, Children, _}) -> {Tag, Attrs, remove_positions(Children)}; remove_positions(Data) -> Data. %% @doc Get node according to a position relative to root node %% @spec get_node_at(ExtendedDoc, Position) -> ExtendedDoc %% @type Position = [non_neg_integer()] %% @type ExtendedDoc = {atom(), [{binary(), any()}], [extended_node()], [non_neg_integer()]} get_node_at(Node, Position) -> get_node_at_aux(Node, lists:reverse(Position)). get_node_at_aux(Node, []) -> Node; get_node_at_aux({_,_,Children,_}, [Pos|Next]) -> get_node_at_aux(lists:nth(Pos, Children), Next). %% @doc Get parent position %% @spec get_parent_position(Position) -> Position %% @type Position = [non_neg_integer()] get_parent_position([_|ParentPosition]) -> ParentPosition. %% @doc Get position relative to my parent %% @spec get_self_position(Position) -> non_neg_integer() %% @type Position = [non_neg_integer()] get_position_in_parent([MyPosition|_]) -> MyPosition. tsung-1.8.0/src/lib/mochiweb_util.erl0000644000201100017670000010145514377756736017277 0ustar nniclausdream%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% @doc Utilities for parsing and quoting. -module(mochiweb_util). -author('bob@mochimedia.com'). -export([join/2, quote_plus/1, urlencode/1, parse_qs/1, unquote/1]). -export([path_split/1]). -export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]). -export([guess_mime/1, parse_header/1]). -export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1, cmd_status/2]). -export([record_to_proplist/2, record_to_proplist/3]). -export([safe_relative_path/1, partition/2]). -export([parse_qvalues/1, pick_accepted_encodings/3]). -export([make_io/1]). -define(PERCENT, 37). % $\% -define(FULLSTOP, 46). % $\. -define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse (C >= $a andalso C =< $f) orelse (C >= $A andalso C =< $F))). -define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse (C >= $A andalso C =< $Z) orelse (C >= $0 andalso C =< $9) orelse (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse C =:= $_))). hexdigit(C) when C < 10 -> $0 + C; hexdigit(C) when C < 16 -> $A + (C - 10). unhexdigit(C) when C >= $0, C =< $9 -> C - $0; unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10; unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10. %% @spec partition(String, Sep) -> {String, [], []} | {Prefix, Sep, Postfix} %% @doc Inspired by Python 2.5's str.partition: %% partition("foo/bar", "/") = {"foo", "/", "bar"}, %% partition("foo", "/") = {"foo", "", ""}. partition(String, Sep) -> case partition(String, Sep, []) of undefined -> {String, "", ""}; Result -> Result end. partition("", _Sep, _Acc) -> undefined; partition(S, Sep, Acc) -> case partition2(S, Sep) of undefined -> [C | Rest] = S, partition(Rest, Sep, [C | Acc]); Rest -> {lists:reverse(Acc), Sep, Rest} end. partition2(Rest, "") -> Rest; partition2([C | R1], [C | R2]) -> partition2(R1, R2); partition2(_S, _Sep) -> undefined. %% @spec safe_relative_path(string()) -> string() | undefined %% @doc Return the reduced version of a relative path or undefined if it %% is not safe. safe relative paths can be joined with an absolute path %% and will result in a subdirectory of the absolute path. Safe paths %% never contain a backslash character. safe_relative_path("/" ++ _) -> undefined; safe_relative_path(P) -> case string:chr(P, $\\) of 0 -> safe_relative_path(P, []); _ -> undefined end. safe_relative_path("", Acc) -> case Acc of [] -> ""; _ -> string:join(lists:reverse(Acc), "/") end; safe_relative_path(P, Acc) -> case partition(P, "/") of {"", "/", _} -> %% /foo or foo//bar undefined; {"..", _, _} when Acc =:= [] -> undefined; {"..", _, Rest} -> safe_relative_path(Rest, tl(Acc)); {Part, "/", ""} -> safe_relative_path("", ["", Part | Acc]); {Part, _, Rest} -> safe_relative_path(Rest, [Part | Acc]) end. %% @spec shell_quote(string()) -> string() %% @doc Quote a string according to UNIX shell quoting rules, returns a string %% surrounded by double quotes. shell_quote(L) -> shell_quote(L, [$\"]). %% @spec cmd_port([string()], Options) -> port() %% @doc open_port({spawn, mochiweb_util:cmd_string(Argv)}, Options). cmd_port(Argv, Options) -> open_port({spawn, cmd_string(Argv)}, Options). %% @spec cmd([string()]) -> string() %% @doc os:cmd(cmd_string(Argv)). cmd(Argv) -> os:cmd(cmd_string(Argv)). %% @spec cmd_string([string()]) -> string() %% @doc Create a shell quoted command string from a list of arguments. cmd_string(Argv) -> string:join([shell_quote(X) || X <- Argv], " "). %% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()} %% @doc Accumulate the output and exit status from the given application, %% will be spawned with cmd_port/2. cmd_status(Argv) -> cmd_status(Argv, []). %% @spec cmd_status([string()], [atom()]) -> {ExitStatus::integer(), Stdout::binary()} %% @doc Accumulate the output and exit status from the given application, %% will be spawned with cmd_port/2. cmd_status(Argv, Options) -> Port = cmd_port(Argv, [exit_status, stderr_to_stdout, use_stdio, binary | Options]), try cmd_loop(Port, []) after catch port_close(Port) end. %% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()} %% @doc Accumulate the output and exit status from a port. cmd_loop(Port, Acc) -> receive {Port, {exit_status, Status}} -> {Status, iolist_to_binary(lists:reverse(Acc))}; {Port, {data, Data}} -> cmd_loop(Port, [Data | Acc]) end. %% @spec join([iolist()], iolist()) -> iolist() %% @doc Join a list of strings or binaries together with the given separator %% string or char or binary. The output is flattened, but may be an %% iolist() instead of a string() if any of the inputs are binary(). join([], _Separator) -> []; join([S], _Separator) -> lists:flatten(S); join(Strings, Separator) -> lists:flatten(revjoin(lists:reverse(Strings), Separator, [])). revjoin([], _Separator, Acc) -> Acc; revjoin([S | Rest], Separator, []) -> revjoin(Rest, Separator, [S]); revjoin([S | Rest], Separator, Acc) -> revjoin(Rest, Separator, [S, Separator | Acc]). %% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string() %% @doc URL safe encoding of the given term. quote_plus(Atom) when is_atom(Atom) -> quote_plus(atom_to_list(Atom)); quote_plus(Int) when is_integer(Int) -> quote_plus(integer_to_list(Int)); quote_plus(Binary) when is_binary(Binary) -> quote_plus(binary_to_list(Binary)); quote_plus(Float) when is_float(Float) -> quote_plus(mochinum:digits(Float)); quote_plus(String) -> quote_plus(String, []). quote_plus([], Acc) -> lists:reverse(Acc); quote_plus([C | Rest], Acc) when ?QS_SAFE(C) -> quote_plus(Rest, [C | Acc]); quote_plus([$\s | Rest], Acc) -> quote_plus(Rest, [$+ | Acc]); quote_plus([C | Rest], Acc) -> <> = <>, quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]). %% @spec urlencode([{Key, Value}]) -> string() %% @doc URL encode the property list. urlencode(Props) -> Pairs = lists:foldr( fun ({K, V}, Acc) -> [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc] end, [], Props), string:join(Pairs, "&"). %% @spec parse_qs(string() | binary()) -> [{Key, Value}] %% @doc Parse a query string or application/x-www-form-urlencoded. parse_qs(Binary) when is_binary(Binary) -> parse_qs(binary_to_list(Binary)); parse_qs(String) -> parse_qs(String, []). parse_qs([], Acc) -> lists:reverse(Acc); parse_qs(String, Acc) -> {Key, Rest} = parse_qs_key(String), {Value, Rest1} = parse_qs_value(Rest), parse_qs(Rest1, [{Key, Value} | Acc]). parse_qs_key(String) -> parse_qs_key(String, []). parse_qs_key([], Acc) -> {qs_revdecode(Acc), ""}; parse_qs_key([$= | Rest], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_key(Rest=[$; | _], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_key(Rest=[$& | _], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_key([C | Rest], Acc) -> parse_qs_key(Rest, [C | Acc]). parse_qs_value(String) -> parse_qs_value(String, []). parse_qs_value([], Acc) -> {qs_revdecode(Acc), ""}; parse_qs_value([$; | Rest], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_value([$& | Rest], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_value([C | Rest], Acc) -> parse_qs_value(Rest, [C | Acc]). %% @spec unquote(string() | binary()) -> string() %% @doc Unquote a URL encoded string. unquote(Binary) when is_binary(Binary) -> unquote(binary_to_list(Binary)); unquote(String) -> qs_revdecode(lists:reverse(String)). qs_revdecode(S) -> qs_revdecode(S, []). qs_revdecode([], Acc) -> Acc; qs_revdecode([$+ | Rest], Acc) -> qs_revdecode(Rest, [$\s | Acc]); qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) -> qs_revdecode(Rest, [(unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4)) | Acc]); qs_revdecode([C | Rest], Acc) -> qs_revdecode(Rest, [C | Acc]). %% @spec urlsplit(Url) -> {Scheme, Netloc, Path, Query, Fragment} %% @doc Return a 5-tuple, does not expand % escapes. Only supports HTTP style %% URLs. urlsplit(Url) -> {Scheme, Url1} = urlsplit_scheme(Url), {Netloc, Url2} = urlsplit_netloc(Url1), {Path, Query, Fragment} = urlsplit_path(Url2), {Scheme, Netloc, Path, Query, Fragment}. urlsplit_scheme(Url) -> case urlsplit_scheme(Url, []) of no_scheme -> {"", Url}; Res -> Res end. urlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse (C >= $A andalso C =< $Z) orelse (C >= $0 andalso C =< $9) orelse C =:= $+ orelse C =:= $- orelse C =:= $.) -> urlsplit_scheme(Rest, [C | Acc]); urlsplit_scheme([$: | Rest], Acc=[_ | _]) -> {string:to_lower(lists:reverse(Acc)), Rest}; urlsplit_scheme(_Rest, _Acc) -> no_scheme. urlsplit_netloc("//" ++ Rest) -> urlsplit_netloc(Rest, []); urlsplit_netloc(Path) -> {"", Path}. urlsplit_netloc("", Acc) -> {lists:reverse(Acc), ""}; urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# -> {lists:reverse(Acc), Rest}; urlsplit_netloc([C | Rest], Acc) -> urlsplit_netloc(Rest, [C | Acc]). %% @spec path_split(string()) -> {Part, Rest} %% @doc Split a path starting from the left, as in URL traversal. %% path_split("foo/bar") = {"foo", "bar"}, %% path_split("/foo/bar") = {"", "foo/bar"}. path_split(S) -> path_split(S, []). path_split("", Acc) -> {lists:reverse(Acc), ""}; path_split("/" ++ Rest, Acc) -> {lists:reverse(Acc), Rest}; path_split([C | Rest], Acc) -> path_split(Rest, [C | Acc]). %% @spec urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> string() %% @doc Assemble a URL from the 5-tuple. Path must be absolute. urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> lists:flatten([case Scheme of "" -> ""; _ -> [Scheme, "://"] end, Netloc, urlunsplit_path({Path, Query, Fragment})]). %% @spec urlunsplit_path({Path, Query, Fragment}) -> string() %% @doc Assemble a URL path from the 3-tuple. urlunsplit_path({Path, Query, Fragment}) -> lists:flatten([Path, case Query of "" -> ""; _ -> [$? | Query] end, case Fragment of "" -> ""; _ -> [$# | Fragment] end]). %% @spec urlsplit_path(Url) -> {Path, Query, Fragment} %% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style %% paths. urlsplit_path(Path) -> urlsplit_path(Path, []). urlsplit_path("", Acc) -> {lists:reverse(Acc), "", ""}; urlsplit_path("?" ++ Rest, Acc) -> {Query, Fragment} = urlsplit_query(Rest), {lists:reverse(Acc), Query, Fragment}; urlsplit_path("#" ++ Rest, Acc) -> {lists:reverse(Acc), "", Rest}; urlsplit_path([C | Rest], Acc) -> urlsplit_path(Rest, [C | Acc]). urlsplit_query(Query) -> urlsplit_query(Query, []). urlsplit_query("", Acc) -> {lists:reverse(Acc), ""}; urlsplit_query("#" ++ Rest, Acc) -> {lists:reverse(Acc), Rest}; urlsplit_query([C | Rest], Acc) -> urlsplit_query(Rest, [C | Acc]). %% @spec guess_mime(string()) -> string() %% @doc Guess the mime type of a file by the extension of its filename. guess_mime(File) -> case filename:basename(File) of "crossdomain.xml" -> "text/x-cross-domain-policy"; Name -> case mochiweb_mime:from_extension(filename:extension(Name)) of undefined -> "text/plain"; Mime -> Mime end end. %% @spec parse_header(string()) -> {Type, [{K, V}]} %% @doc Parse a Content-Type like header, return the main Content-Type %% and a property list of options. parse_header(String) -> %% TODO: This is exactly as broken as Python's cgi module. %% Should parse properly like mochiweb_cookies. [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")], F = fun (S, Acc) -> case lists:splitwith(fun (C) -> C =/= $= end, S) of {"", _} -> %% Skip anything with no name Acc; {_, ""} -> %% Skip anything with no value Acc; {Name, [$\= | Value]} -> [{string:to_lower(string:strip(Name)), unquote_header(string:strip(Value))} | Acc] end end, {string:to_lower(Type), lists:foldr(F, [], Parts)}. unquote_header("\"" ++ Rest) -> unquote_header(Rest, []); unquote_header(S) -> S. unquote_header("", Acc) -> lists:reverse(Acc); unquote_header("\"", Acc) -> lists:reverse(Acc); unquote_header([$\\, C | Rest], Acc) -> unquote_header(Rest, [C | Acc]); unquote_header([C | Rest], Acc) -> unquote_header(Rest, [C | Acc]). %% @spec record_to_proplist(Record, Fields) -> proplist() %% @doc calls record_to_proplist/3 with a default TypeKey of '__record' record_to_proplist(Record, Fields) -> record_to_proplist(Record, Fields, '__record'). %% @spec record_to_proplist(Record, Fields, TypeKey) -> proplist() %% @doc Return a proplist of the given Record with each field in the %% Fields list set as a key with the corresponding value in the Record. %% TypeKey is the key that is used to store the record type %% Fields should be obtained by calling record_info(fields, record_type) %% where record_type is the record type of Record record_to_proplist(Record, Fields, TypeKey) when tuple_size(Record) - 1 =:= length(Fields) -> lists:zip([TypeKey | Fields], tuple_to_list(Record)). shell_quote([], Acc) -> lists:reverse([$\" | Acc]); shell_quote([C | Rest], Acc) when C =:= $\" orelse C =:= $\` orelse C =:= $\\ orelse C =:= $\$ -> shell_quote(Rest, [C, $\\ | Acc]); shell_quote([C | Rest], Acc) -> shell_quote(Rest, [C | Acc]). %% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string %% @type qvalue() = {media_type() | encoding() , float()}. %% @type media_type() = string(). %% @type encoding() = string(). %% %% @doc Parses a list (given as a string) of elements with Q values associated %% to them. Elements are separated by commas and each element is separated %% from its Q value by a semicolon. Q values are optional but when missing %% the value of an element is considered as 1.0. A Q value is always in the %% range [0.0, 1.0]. A Q value list is used for example as the value of the %% HTTP "Accept" and "Accept-Encoding" headers. %% %% Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1). %% %% Example: %% %% parse_qvalues("gzip; q=0.5, deflate, identity;q=0.0") -> %% [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] %% parse_qvalues(QValuesStr) -> try lists:map( fun(Pair) -> [Type | Params] = string:tokens(Pair, ";"), NormParams = normalize_media_params(Params), {Q, NonQParams} = extract_q(NormParams), {string:join([string:strip(Type) | NonQParams], ";"), Q} end, string:tokens(string:to_lower(QValuesStr), ",") ) catch _Type:_Error -> invalid_qvalue_string end. normalize_media_params(Params) -> {ok, Re} = re:compile("\\s"), normalize_media_params(Re, Params, []). normalize_media_params(_Re, [], Acc) -> lists:reverse(Acc); normalize_media_params(Re, [Param | Rest], Acc) -> NormParam = re:replace(Param, Re, "", [global, {return, list}]), normalize_media_params(Re, Rest, [NormParam | Acc]). extract_q(NormParams) -> {ok, KVRe} = re:compile("^([^=]+)=([^=]+)$"), {ok, QRe} = re:compile("^((?:0|1)(?:\\.\\d{1,3})?)$"), extract_q(KVRe, QRe, NormParams, []). extract_q(_KVRe, _QRe, [], Acc) -> {1.0, lists:reverse(Acc)}; extract_q(KVRe, QRe, [Param | Rest], Acc) -> case re:run(Param, KVRe, [{capture, [1, 2], list}]) of {match, [Name, Value]} -> case Name of "q" -> {match, [Q]} = re:run(Value, QRe, [{capture, [1], list}]), QVal = case Q of "0" -> 0.0; "1" -> 1.0; Else -> list_to_float(Else) end, case QVal < 0.0 orelse QVal > 1.0 of false -> {QVal, lists:reverse(Acc) ++ Rest} end; _ -> extract_q(KVRe, QRe, Rest, [Param | Acc]) end end. %% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) -> %% [encoding()] %% %% @doc Determines which encodings specified in the given Q values list are %% valid according to a list of supported encodings and a default encoding. %% %% The returned list of encodings is sorted, descendingly, according to the %% Q values of the given list. The last element of this list is the given %% default encoding unless this encoding is explicitly or implicitily %% marked with a Q value of 0.0 in the given Q values list. %% Note: encodings with the same Q value are kept in the same order as %% found in the input Q values list. %% %% This encoding picking process is described in section 14.3 of the %% RFC 2616 (HTTP 1.1). %% %% Example: %% %% pick_accepted_encodings( %% [{"gzip", 0.5}, {"deflate", 1.0}], %% ["gzip", "identity"], %% "identity" %% ) -> %% ["gzip", "identity"] %% pick_accepted_encodings(AcceptedEncs, SupportedEncs, DefaultEnc) -> SortedQList = lists:reverse( lists:sort(fun({_, Q1}, {_, Q2}) -> Q1 < Q2 end, AcceptedEncs) ), {Accepted, Refused} = lists:foldr( fun({E, Q}, {A, R}) -> case Q > 0.0 of true -> {[E | A], R}; false -> {A, [E | R]} end end, {[], []}, SortedQList ), Refused1 = lists:foldr( fun(Enc, Acc) -> case Enc of "*" -> lists:subtract(SupportedEncs, Accepted) ++ Acc; _ -> [Enc | Acc] end end, [], Refused ), Accepted1 = lists:foldr( fun(Enc, Acc) -> case Enc of "*" -> lists:subtract(SupportedEncs, Accepted ++ Refused1) ++ Acc; _ -> [Enc | Acc] end end, [], Accepted ), Accepted2 = case lists:member(DefaultEnc, Accepted1) of true -> Accepted1; false -> Accepted1 ++ [DefaultEnc] end, [E || E <- Accepted2, lists:member(E, SupportedEncs), not lists:member(E, Refused1)]. make_io(Atom) when is_atom(Atom) -> atom_to_list(Atom); make_io(Integer) when is_integer(Integer) -> integer_to_list(Integer); make_io(Io) when is_list(Io); is_binary(Io) -> Io. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). make_io_test() -> ?assertEqual( <<"atom">>, iolist_to_binary(make_io(atom))), ?assertEqual( <<"20">>, iolist_to_binary(make_io(20))), ?assertEqual( <<"list">>, iolist_to_binary(make_io("list"))), ?assertEqual( <<"binary">>, iolist_to_binary(make_io(<<"binary">>))), ok. -record(test_record, {field1=f1, field2=f2}). record_to_proplist_test() -> ?assertEqual( [{'__record', test_record}, {field1, f1}, {field2, f2}], record_to_proplist(#test_record{}, record_info(fields, test_record))), ?assertEqual( [{'typekey', test_record}, {field1, f1}, {field2, f2}], record_to_proplist(#test_record{}, record_info(fields, test_record), typekey)), ok. shell_quote_test() -> ?assertEqual( "\"foo \\$bar\\\"\\`' baz\"", shell_quote("foo $bar\"`' baz")), ok. cmd_port_test_spool(Port, Acc) -> receive {Port, eof} -> Acc; {Port, {data, {eol, Data}}} -> cmd_port_test_spool(Port, ["\n", Data | Acc]); {Port, Unknown} -> throw({unknown, Unknown}) after 1000 -> throw(timeout) end. cmd_port_test() -> Port = cmd_port(["echo", "$bling$ `word`!"], [eof, stream, {line, 4096}]), Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, []))) after catch port_close(Port) end, self() ! {Port, wtf}, try cmd_port_test_spool(Port, []) catch throw:{unknown, wtf} -> ok end, try cmd_port_test_spool(Port, []) catch throw:timeout -> ok end, ?assertEqual( "$bling$ `word`!\n", Res). cmd_test() -> ?assertEqual( "$bling$ `word`!\n", cmd(["echo", "$bling$ `word`!"])), ok. cmd_string_test() -> ?assertEqual( "\"echo\" \"\\$bling\\$ \\`word\\`!\"", cmd_string(["echo", "$bling$ `word`!"])), ok. cmd_status_test() -> ?assertEqual( {0, <<"$bling$ `word`!\n">>}, cmd_status(["echo", "$bling$ `word`!"])), ok. parse_header_test() -> ?assertEqual( {"multipart/form-data", [{"boundary", "AaB03x"}]}, parse_header("multipart/form-data; boundary=AaB03x")), %% This tests (currently) intentionally broken behavior ?assertEqual( {"multipart/form-data", [{"b", ""}, {"cgi", "is"}, {"broken", "true\"e"}]}, parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")), ok. guess_mime_test() -> ?assertEqual("text/plain", guess_mime("")), ?assertEqual("text/plain", guess_mime(".text")), ?assertEqual("application/zip", guess_mime(".zip")), ?assertEqual("application/zip", guess_mime("x.zip")), ?assertEqual("text/html", guess_mime("x.html")), ?assertEqual("application/xhtml+xml", guess_mime("x.xhtml")), ?assertEqual("text/x-cross-domain-policy", guess_mime("crossdomain.xml")), ?assertEqual("text/x-cross-domain-policy", guess_mime("www/crossdomain.xml")), ok. path_split_test() -> {"", "foo/bar"} = path_split("/foo/bar"), {"foo", "bar"} = path_split("foo/bar"), {"bar", ""} = path_split("bar"), ok. urlsplit_test() -> {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"), {"http", "host:port", "/foo", "", "bar?baz"} = urlsplit("http://host:port/foo#bar?baz"), {"http", "host", "", "", ""} = urlsplit("http://host"), {"", "", "/wiki/Category:Fruit", "", ""} = urlsplit("/wiki/Category:Fruit"), ok. urlsplit_path_test() -> {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"), {"/foo", "baz", ""} = urlsplit_path("/foo?baz"), {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"), {"/foo", "", "bar?baz#wibble"} = urlsplit_path("/foo#bar?baz#wibble"), {"/foo", "bar", "baz"} = urlsplit_path("/foo?bar#baz"), {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"), ok. urlunsplit_test() -> "/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}), "http://host:port/foo#bar?baz" = urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}), ok. urlunsplit_path_test() -> "/foo/bar" = urlunsplit_path({"/foo/bar", "", ""}), "/foo?baz" = urlunsplit_path({"/foo", "baz", ""}), "/foo#bar?baz" = urlunsplit_path({"/foo", "", "bar?baz"}), "/foo#bar?baz#wibble" = urlunsplit_path({"/foo", "", "bar?baz#wibble"}), "/foo?bar#baz" = urlunsplit_path({"/foo", "bar", "baz"}), "/foo?bar?baz#baz" = urlunsplit_path({"/foo", "bar?baz", "baz"}), ok. join_test() -> ?assertEqual("foo,bar,baz", join(["foo", "bar", "baz"], $,)), ?assertEqual("foo,bar,baz", join(["foo", "bar", "baz"], ",")), ?assertEqual("foo bar", join([["foo", " bar"]], ",")), ?assertEqual("foo bar,baz", join([["foo", " bar"], "baz"], ",")), ?assertEqual("foo", join(["foo"], ",")), ?assertEqual("foobarbaz", join(["foo", "bar", "baz"], "")), ?assertEqual("foo" ++ [<<>>] ++ "bar" ++ [<<>>] ++ "baz", join(["foo", "bar", "baz"], <<>>)), ?assertEqual("foobar" ++ [<<"baz">>], join(["foo", "bar", <<"baz">>], "")), ?assertEqual("", join([], "any")), ok. quote_plus_test() -> "foo" = quote_plus(foo), "1" = quote_plus(1), "1.1" = quote_plus(1.1), "foo" = quote_plus("foo"), "foo+bar" = quote_plus("foo bar"), "foo%0A" = quote_plus("foo\n"), "foo%0A" = quote_plus("foo\n"), "foo%3B%26%3D" = quote_plus("foo;&="), "foo%3B%26%3D" = quote_plus(<<"foo;&=">>), ok. unquote_test() -> ?assertEqual("foo bar", unquote("foo+bar")), ?assertEqual("foo bar", unquote("foo%20bar")), ?assertEqual("foo\r\n", unquote("foo%0D%0A")), ?assertEqual("foo\r\n", unquote(<<"foo%0D%0A">>)), ok. urlencode_test() -> "foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"}, {"baz", "wibble \r\n"}, {z, 1}]), ok. parse_qs_test() -> ?assertEqual( [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}], parse_qs("foo=bar&baz=wibble+%0D%0a&z=1")), ?assertEqual( [{"", "bar"}, {"baz", "wibble \r\n"}, {"z", ""}], parse_qs("=bar&baz=wibble+%0D%0a&z=")), ?assertEqual( [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}], parse_qs(<<"foo=bar&baz=wibble+%0D%0a&z=1">>)), ?assertEqual( [], parse_qs("")), ?assertEqual( [{"foo", ""}, {"bar", ""}, {"baz", ""}], parse_qs("foo;bar&baz")), ok. partition_test() -> {"foo", "", ""} = partition("foo", "/"), {"foo", "/", "bar"} = partition("foo/bar", "/"), {"foo", "/", ""} = partition("foo/", "/"), {"", "/", "bar"} = partition("/bar", "/"), {"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"), ok. safe_relative_path_test() -> "foo" = safe_relative_path("foo"), "foo/" = safe_relative_path("foo/"), "foo" = safe_relative_path("foo/bar/.."), "bar" = safe_relative_path("foo/../bar"), "bar/" = safe_relative_path("foo/../bar/"), "" = safe_relative_path("foo/.."), "" = safe_relative_path("foo/../"), undefined = safe_relative_path("/foo"), undefined = safe_relative_path("../foo"), undefined = safe_relative_path("foo/../.."), undefined = safe_relative_path("foo//"), undefined = safe_relative_path("foo\\bar"), ok. parse_qvalues_test() -> [] = parse_qvalues(""), [{"identity", 0.0}] = parse_qvalues("identity;q=0"), [{"identity", 0.0}] = parse_qvalues("identity ;q=0"), [{"identity", 0.0}] = parse_qvalues(" identity; q =0 "), [{"identity", 0.0}] = parse_qvalues("identity ; q = 0"), [{"identity", 0.0}] = parse_qvalues("identity ; q= 0.0"), [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip,deflate,identity;q=0.0" ), [{"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues( "deflate,gzip,identity;q=0.0" ), [{"gzip", 1.0}, {"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues("gzip,deflate,gzip,identity;q=0"), [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip, deflate , identity; q=0.0" ), [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip; q=1, deflate;q=1.0, identity;q=0.0" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip; q=0.5, deflate;q=1.0, identity;q=0" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip; q=0.5, deflate , identity;q=0.0" ), [{"gzip", 0.5}, {"deflate", 0.8}, {"identity", 0.0}] = parse_qvalues( "gzip; q=0.5, deflate;q=0.8, identity;q=0.0" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}] = parse_qvalues( "gzip; q=0.5,deflate,identity" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}, {"identity", 1.0}] = parse_qvalues("gzip; q=0.5,deflate,identity, identity "), [{"text/html;level=1", 1.0}, {"text/plain", 0.5}] = parse_qvalues("text/html;level=1, text/plain;q=0.5"), [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = parse_qvalues("text/html;level=1;q=0.3, text/plain"), [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = parse_qvalues("text/html; level = 1; q = 0.3, text/plain"), [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = parse_qvalues("text/html;q=0.3;level=1, text/plain"), invalid_qvalue_string = parse_qvalues("gzip; q=1.1, deflate"), invalid_qvalue_string = parse_qvalues("gzip; q=0.5, deflate;q=2"), invalid_qvalue_string = parse_qvalues("gzip, deflate;q=AB"), invalid_qvalue_string = parse_qvalues("gzip; q=2.1, deflate"), invalid_qvalue_string = parse_qvalues("gzip; q=0.1234, deflate"), invalid_qvalue_string = parse_qvalues("text/html;level=1;q=0.3, text/html;level"), ok. pick_accepted_encodings_test() -> ["identity"] = pick_accepted_encodings( [], ["gzip", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"gzip", 1.0}], ["gzip", "identity"], "identity" ), ["identity"] = pick_accepted_encodings( [{"gzip", 0.0}], ["gzip", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}], ["gzip", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.5}, {"deflate", 1.0}], ["gzip", "identity"], "identity" ), ["identity"] = pick_accepted_encodings( [{"gzip", 0.0}, {"deflate", 0.0}], ["gzip", "identity"], "identity" ), ["gzip"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}], ["gzip", "identity"], "identity" ), ["gzip", "deflate", "identity"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "deflate", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 1.0}, {"deflate", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "gzip", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 1.0}, {"gzip", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 0.9}, {"gzip", 1.0}], ["gzip", "deflate", "identity"], "identity" ), [] = pick_accepted_encodings( [{"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate", "identity"] = pick_accepted_encodings( [{"*", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate", "identity"] = pick_accepted_encodings( [{"*", 0.6}], ["gzip", "deflate", "identity"], "identity" ), ["gzip"] = pick_accepted_encodings( [{"gzip", 1.0}, {"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 0.6}, {"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "gzip"] = pick_accepted_encodings( [{"gzip", 0.5}, {"deflate", 1.0}, {"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"deflate", 0.0}, {"*", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"*", 1.0}, {"deflate", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ok. -endif. tsung-1.8.0/src/lib/mochiweb_html.erl0000644000201100017670000007117314377756736017271 0ustar nniclausdream%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Loosely tokenizes and generates parse trees for HTML 4. -module(mochiweb_html). -export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1, escape_attr/1, to_html/1]). -compile([export_all]). -ifdef(TEST). -export([destack/1, destack/2, is_singleton/1]). -endif. %% This is a macro to placate syntax highlighters.. -define(QUOTE, $\"). %% $\" -define(SQUOTE, $\'). %% $\' -define(ADV_COL(S, N), S#decoder{column=N+S#decoder.column, offset=N+S#decoder.offset}). -define(INC_COL(S), S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset}). -define(INC_LINE(S), S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}). -define(INC_CHAR(S, C), case C of $\n -> S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}; _ -> S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset} end). -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). -define(IS_LITERAL_SAFE(C), ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z) orelse (C >= $0 andalso C =< $9))). -define(PROBABLE_CLOSE(C), (C =:= $> orelse ?IS_WHITESPACE(C))). -record(decoder, {line=1, column=1, offset=0}). %% @type html_node() = {string(), [html_attr()], [html_node() | string()]} %% @type html_attr() = {string(), string()} %% @type html_token() = html_data() | start_tag() | end_tag() | inline_html() | html_comment() | html_doctype() %% @type html_data() = {data, string(), Whitespace::boolean()} %% @type start_tag() = {start_tag, Name, [html_attr()], Singleton::boolean()} %% @type end_tag() = {end_tag, Name} %% @type html_comment() = {comment, Comment} %% @type html_doctype() = {doctype, [Doctype]} %% @type inline_html() = {'=', iolist()} %% External API. %% @spec parse(string() | binary()) -> html_node() %% @doc tokenize and then transform the token stream into a HTML tree. parse(Input) -> parse_tokens(tokens(Input)). %% @spec parse_tokens([html_token()]) -> html_node() %% @doc Transform the output of tokens(Doc) into a HTML tree. parse_tokens(Tokens) when is_list(Tokens) -> %% Skip over doctype, processing instructions [{start_tag, Tag, Attrs, false} | Rest] = find_document(Tokens, normal), {Tree, _} = tree(Rest, [norm({Tag, Attrs})]), Tree. find_document(Tokens=[{start_tag, _Tag, _Attrs, false} | _Rest], Mode) -> maybe_add_html_tag(Tokens, Mode); find_document([{doctype, [<<"html">>]} | Rest], _Mode) -> find_document(Rest, html5); find_document([_T | Rest], Mode) -> find_document(Rest, Mode); find_document([], _Mode) -> []. maybe_add_html_tag(Tokens=[{start_tag, Tag, _Attrs, false} | _], html5) when Tag =/= <<"html">> -> [{start_tag, <<"html">>, [], false} | Tokens]; maybe_add_html_tag(Tokens, _Mode) -> Tokens. %% @spec tokens(StringOrBinary) -> [html_token()] %% @doc Transform the input UTF-8 HTML into a token stream. tokens(Input) -> tokens(iolist_to_binary(Input), #decoder{}, []). %% @spec to_tokens(html_node()) -> [html_token()] %% @doc Convert a html_node() tree to a list of tokens. to_tokens({Tag0}) -> to_tokens({Tag0, [], []}); to_tokens(T={'=', _}) -> [T]; to_tokens(T={doctype, _}) -> [T]; to_tokens(T={comment, _}) -> [T]; to_tokens({Tag0, Acc}) -> %% This is only allowed in sub-tags: {p, [{"class", "foo"}]} to_tokens({Tag0, [], Acc}); to_tokens({Tag0, Attrs, Acc}) -> Tag = to_tag(Tag0), case is_singleton(Tag) of true -> to_tokens([], [{start_tag, Tag, Attrs, true}]); false -> to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, false}]) end. %% @spec to_html([html_token()] | html_node()) -> iolist() %% @doc Convert a list of html_token() to a HTML document. to_html(Node) when is_tuple(Node) -> to_html(to_tokens(Node)); to_html(Tokens) when is_list(Tokens) -> to_html(Tokens, []). %% @spec escape(string() | atom() | binary()) -> binary() %% @doc Escape a string such that it's safe for HTML (amp; lt; gt;). escape(B) when is_binary(B) -> escape(binary_to_list(B), []); escape(A) when is_atom(A) -> escape(atom_to_list(A), []); escape(S) when is_list(S) -> escape(S, []). %% @spec escape_attr(string() | binary() | atom() | integer() | float()) -> binary() %% @doc Escape a string such that it's safe for HTML attrs %% (amp; lt; gt; quot;). escape_attr(B) when is_binary(B) -> escape_attr(binary_to_list(B), []); escape_attr(A) when is_atom(A) -> escape_attr(atom_to_list(A), []); escape_attr(S) when is_list(S) -> escape_attr(S, []); escape_attr(I) when is_integer(I) -> escape_attr(integer_to_list(I), []); escape_attr(F) when is_float(F) -> escape_attr(mochinum:digits(F), []). to_html([], Acc) -> lists:reverse(Acc); to_html([{'=', Content} | Rest], Acc) -> to_html(Rest, [Content | Acc]); to_html([{pi, Bin} | Rest], Acc) -> Open = [<<">, Bin, <<"?>">>], to_html(Rest, [Open | Acc]); to_html([{pi, Tag, Attrs} | Rest], Acc) -> Open = [<<">, Tag, attrs_to_html(Attrs, []), <<"?>">>], to_html(Rest, [Open | Acc]); to_html([{comment, Comment} | Rest], Acc) -> to_html(Rest, [[<<"">>] | Acc]); to_html([{doctype, Parts} | Rest], Acc) -> Inside = doctype_to_html(Parts, Acc), to_html(Rest, [[<<">, Inside, <<">">>] | Acc]); to_html([{data, Data, _Whitespace} | Rest], Acc) -> to_html(Rest, [escape(Data) | Acc]); to_html([{start_tag, Tag, Attrs, Singleton} | Rest], Acc) -> Open = [<<"<">>, Tag, attrs_to_html(Attrs, []), case Singleton of true -> <<" />">>; false -> <<">">> end], to_html(Rest, [Open | Acc]); to_html([{end_tag, Tag} | Rest], Acc) -> to_html(Rest, [[<<">, Tag, <<">">>] | Acc]). doctype_to_html([], Acc) -> lists:reverse(Acc); doctype_to_html([Word | Rest], Acc) -> case lists:all(fun (C) -> ?IS_LITERAL_SAFE(C) end, binary_to_list(iolist_to_binary(Word))) of true -> doctype_to_html(Rest, [[<<" ">>, Word] | Acc]); false -> doctype_to_html(Rest, [[<<" \"">>, escape_attr(Word), ?QUOTE] | Acc]) end. attrs_to_html([], Acc) -> lists:reverse(Acc); attrs_to_html([{K, V} | Rest], Acc) -> attrs_to_html(Rest, [[<<" ">>, escape(K), <<"=\"">>, escape_attr(V), <<"\"">>] | Acc]). escape([], Acc) -> list_to_binary(lists:reverse(Acc)); escape("<" ++ Rest, Acc) -> escape(Rest, lists:reverse("<", Acc)); escape(">" ++ Rest, Acc) -> escape(Rest, lists:reverse(">", Acc)); escape("&" ++ Rest, Acc) -> escape(Rest, lists:reverse("&", Acc)); escape([C | Rest], Acc) -> escape(Rest, [C | Acc]). escape_attr([], Acc) -> list_to_binary(lists:reverse(Acc)); escape_attr("<" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse("<", Acc)); escape_attr(">" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse(">", Acc)); escape_attr("&" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse("&", Acc)); escape_attr([?QUOTE | Rest], Acc) -> escape_attr(Rest, lists:reverse(""", Acc)); escape_attr([C | Rest], Acc) -> escape_attr(Rest, [C | Acc]). to_tag(A) when is_atom(A) -> norm(atom_to_list(A)); to_tag(L) -> norm(L). to_tokens([], Acc) -> lists:reverse(Acc); to_tokens([{Tag, []} | Rest], Acc) -> to_tokens(Rest, [{end_tag, to_tag(Tag)} | Acc]); to_tokens([{Tag0, [{T0} | R1]} | Rest], Acc) -> %% Allow {br} to_tokens([{Tag0, [{T0, [], []} | R1]} | Rest], Acc); to_tokens([{Tag0, [T0={'=', _C0} | R1]} | Rest], Acc) -> %% Allow {'=', iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc) -> %% Allow {comment, iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0} | R1]} | Rest], Acc) -> %% Allow {pi, binary()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0, _A0} | R1]} | Rest], Acc) -> %% Allow {pi, binary(), list()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [{T0, A0=[{_, _} | _]} | R1]} | Rest], Acc) -> %% Allow {p, [{"class", "foo"}]} to_tokens([{Tag0, [{T0, A0, []} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, C0} | R1]} | Rest], Acc) -> %% Allow {p, "content"} and {p, <<"content">>} to_tokens([{Tag0, [{T0, [], C0} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C0} | R1]} | Rest], Acc) when is_binary(C0) -> %% Allow {"p", [{"class", "foo"}], <<"content">>} to_tokens([{Tag0, [{T0, A1, binary_to_list(C0)} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C0=[C | _]} | R1]} | Rest], Acc) when is_integer(C) -> %% Allow {"p", [{"class", "foo"}], "content"} to_tokens([{Tag0, [{T0, A1, [C0]} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C1} | R1]} | Rest], Acc) -> %% Native {"p", [{"class", "foo"}], ["content"]} Tag = to_tag(Tag0), T1 = to_tag(T0), case is_singleton(norm(T1)) of true -> to_tokens([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc]); false -> to_tokens([{T1, C1}, {Tag, R1} | Rest], [{start_tag, T1, A1, false} | Acc]) end; to_tokens([{Tag0, [L | R1]} | Rest], Acc) when is_list(L) -> %% List text Tag = to_tag(Tag0), to_tokens([{Tag, R1} | Rest], [{data, iolist_to_binary(L), false} | Acc]); to_tokens([{Tag0, [B | R1]} | Rest], Acc) when is_binary(B) -> %% Binary text Tag = to_tag(Tag0), to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]). tokens(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary>> -> lists:reverse(Acc); _ -> {Tag, S1} = tokenize(B, S), case parse_flag(Tag) of script -> {Tag2, S2} = tokenize_script(B, S1), tokens(B, S2, [Tag2, Tag | Acc]); textarea -> {Tag2, S2} = tokenize_textarea(B, S1), tokens(B, S2, [Tag2, Tag | Acc]); none -> tokens(B, S1, [Tag | Acc]) end end. parse_flag({start_tag, B, _, false}) -> case string:to_lower(binary_to_list(B)) of "script" -> script; "textarea" -> textarea; _ -> none end; parse_flag(_) -> none. tokenize(B, S=#decoder{offset=O}) -> case B of <<_:O/binary, "", _/binary>> -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{comment, Raw}, ?ADV_COL(S, 3)}; <<_:O/binary, C, _/binary>> -> tokenize_comment(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{comment, Raw}, S} end. tokenize_script(Bin, S=#decoder{offset=O}) -> tokenize_script(Bin, S, O). tokenize_script(Bin, S=#decoder{offset=O}, Start) -> case Bin of %% Just a look-ahead, we want the end_tag separately <<_:O/binary, $<, $/, SS, CC, RR, II, PP, TT, ZZ, _/binary>> when (SS =:= $s orelse SS =:= $S) andalso (CC =:= $c orelse CC =:= $C) andalso (RR =:= $r orelse RR =:= $R) andalso (II =:= $i orelse II =:= $I) andalso (PP =:= $p orelse PP =:= $P) andalso (TT=:= $t orelse TT =:= $T) andalso ?PROBABLE_CLOSE(ZZ) -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{data, Raw, false}, S}; <<_:O/binary, C, _/binary>> -> tokenize_script(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{data, Raw, false}, S} end. tokenize_textarea(Bin, S=#decoder{offset=O}) -> tokenize_textarea(Bin, S, O). tokenize_textarea(Bin, S=#decoder{offset=O}, Start) -> case Bin of %% Just a look-ahead, we want the end_tag separately <<_:O/binary, $<, $/, TT, EE, XX, TT2, AA, RR, EE2, AA2, ZZ, _/binary>> when (TT =:= $t orelse TT =:= $T) andalso (EE =:= $e orelse EE =:= $E) andalso (XX =:= $x orelse XX =:= $X) andalso (TT2 =:= $t orelse TT2 =:= $T) andalso (AA =:= $a orelse AA =:= $A) andalso (RR =:= $r orelse RR =:= $R) andalso (EE2 =:= $e orelse EE2 =:= $E) andalso (AA2 =:= $a orelse AA2 =:= $A) andalso ?PROBABLE_CLOSE(ZZ) -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{data, Raw, false}, S}; <<_:O/binary, C, _/binary>> -> tokenize_textarea(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{data, Raw, false}, S} end. tsung-1.8.0/src/lib/mochiweb_headers.erl0000644000201100017670000003725014377756736017736 0ustar nniclausdream%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Case preserving (but case insensitive) HTTP Header dictionary. -module(mochiweb_headers). -author('bob@mochimedia.com'). -export([empty/0, from_list/1, insert/3, enter/3, get_value/2, lookup/2]). -export([delete_any/2, get_primary_value/2, get_combined_value/2]). -export([default/3, enter_from_list/2, default_from_list/2]). -export([to_list/1, make/1]). -export([from_binary/1]). %% @type headers(). %% @type key() = atom() | binary() | string(). %% @type value() = atom() | binary() | string() | integer(). %% @spec empty() -> headers() %% @doc Create an empty headers structure. empty() -> gb_trees:empty(). %% @spec make(headers() | [{key(), value()}]) -> headers() %% @doc Construct a headers() from the given list. make(L) when is_list(L) -> from_list(L); %% assume a non-list is already mochiweb_headers. make(T) -> T. %% @spec from_binary(iolist()) -> headers() %% @doc Transforms a raw HTTP header into a mochiweb headers structure. %% %% The given raw HTTP header can be one of the following: %% %% 1) A string or a binary representing a full HTTP header ending with %% double CRLF. %% Examples: %% ``` %% "Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n" %% <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>''' %% %% 2) A list of binaries or strings where each element represents a raw %% HTTP header line ending with a single CRLF. %% Examples: %% ``` %% [<<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">>] %% ["Content-Length: 47\r\n", "Content-Type: text/plain\r\n"] %% ["Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">>]''' %% from_binary(RawHttpHeader) when is_binary(RawHttpHeader) -> from_binary(RawHttpHeader, []); from_binary(RawHttpHeaderList) -> from_binary(list_to_binary([RawHttpHeaderList, "\r\n"])). from_binary(RawHttpHeader, Acc) -> case erlang:decode_packet(httph, RawHttpHeader, []) of {ok, {http_header, _, H, _, V}, Rest} -> from_binary(Rest, [{H, V} | Acc]); _ -> make(Acc) end. %% @spec from_list([{key(), value()}]) -> headers() %% @doc Construct a headers() from the given list. from_list(List) -> lists:foldl(fun ({K, V}, T) -> insert(K, V, T) end, empty(), List). %% @spec enter_from_list([{key(), value()}], headers()) -> headers() %% @doc Insert pairs into the headers, replace any values for existing keys. enter_from_list(List, T) -> lists:foldl(fun ({K, V}, T1) -> enter(K, V, T1) end, T, List). %% @spec default_from_list([{key(), value()}], headers()) -> headers() %% @doc Insert pairs into the headers for keys that do not already exist. default_from_list(List, T) -> lists:foldl(fun ({K, V}, T1) -> default(K, V, T1) end, T, List). %% @spec to_list(headers()) -> [{key(), string()}] %% @doc Return the contents of the headers. The keys will be the exact key %% that was first inserted (e.g. may be an atom or binary, case is %% preserved). to_list(T) -> F = fun ({K, {array, L}}, Acc) -> L1 = lists:reverse(L), lists:foldl(fun (V, Acc1) -> [{K, V} | Acc1] end, Acc, L1); (Pair, Acc) -> [Pair | Acc] end, lists:reverse(lists:foldl(F, [], gb_trees:values(T))). %% @spec get_value(key(), headers()) -> string() | undefined %% @doc Return the value of the given header using a case insensitive search. %% undefined will be returned for keys that are not present. get_value(K, T) -> case lookup(K, T) of {value, {_, V}} -> expand(V); none -> undefined end. %% @spec get_primary_value(key(), headers()) -> string() | undefined %% @doc Return the value of the given header up to the first semicolon using %% a case insensitive search. undefined will be returned for keys %% that are not present. get_primary_value(K, T) -> case get_value(K, T) of undefined -> undefined; V -> lists:takewhile(fun (C) -> C =/= $; end, V) end. %% @spec get_combined_value(key(), headers()) -> string() | undefined %% @doc Return the value from the given header using a case insensitive search. %% If the value of the header is a comma-separated list where holds values %% are all identical, the identical value will be returned. %% undefined will be returned for keys that are not present or the %% values in the list are not the same. %% %% NOTE: The process isn't designed for a general purpose. If you need %% to access all values in the combined header, please refer to %% '''tokenize_header_value/1'''. %% %% Section 4.2 of the RFC 2616 (HTTP 1.1) describes multiple message-header %% fields with the same field-name may be present in a message if and only %% if the entire field-value for that header field is defined as a %% comma-separated list [i.e., #(values)]. get_combined_value(K, T) -> case get_value(K, T) of undefined -> undefined; V -> case sets:to_list(sets:from_list(tokenize_header_value(V))) of [Val] -> Val; _ -> undefined end end. %% @spec lookup(key(), headers()) -> {value, {key(), string()}} | none %% @doc Return the case preserved key and value for the given header using %% a case insensitive search. none will be returned for keys that are %% not present. lookup(K, T) -> case gb_trees:lookup(normalize(K), T) of {value, {K0, V}} -> {value, {K0, expand(V)}}; none -> none end. %% @spec default(key(), value(), headers()) -> headers() %% @doc Insert the pair into the headers if it does not already exist. default(K, V, T) -> K1 = normalize(K), V1 = any_to_list(V), try gb_trees:insert(K1, {K, V1}, T) catch error:{key_exists, _} -> T end. %% @spec enter(key(), value(), headers()) -> headers() %% @doc Insert the pair into the headers, replacing any pre-existing key. enter(K, V, T) -> K1 = normalize(K), V1 = any_to_list(V), gb_trees:enter(K1, {K, V1}, T). %% @spec insert(key(), value(), headers()) -> headers() %% @doc Insert the pair into the headers, merging with any pre-existing key. %% A merge is done with Value = V0 ++ ", " ++ V1. insert(K, V, T) -> K1 = normalize(K), V1 = any_to_list(V), try gb_trees:insert(K1, {K, V1}, T) catch error:{key_exists, _} -> {K0, V0} = gb_trees:get(K1, T), V2 = merge(K1, V1, V0), gb_trees:update(K1, {K0, V2}, T) end. %% @spec delete_any(key(), headers()) -> headers() %% @doc Delete the header corresponding to key if it is present. delete_any(K, T) -> K1 = normalize(K), gb_trees:delete_any(K1, T). %% Internal API tokenize_header_value(undefined) -> undefined; tokenize_header_value(V) -> reversed_tokens(trim_and_reverse(V, false), [], []). trim_and_reverse([S | Rest], Reversed) when S=:=$ ; S=:=$\n; S=:=$\t -> trim_and_reverse(Rest, Reversed); trim_and_reverse(V, false) -> trim_and_reverse(lists:reverse(V), true); trim_and_reverse(V, true) -> V. reversed_tokens([], [], Acc) -> Acc; reversed_tokens([], Token, Acc) -> [Token | Acc]; reversed_tokens("\"" ++ Rest, [], Acc) -> case extract_quoted_string(Rest, []) of {String, NewRest} -> reversed_tokens(NewRest, [], [String | Acc]); undefined -> undefined end; reversed_tokens("\"" ++ _Rest, _Token, _Acc) -> undefined; reversed_tokens([C | Rest], [], Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, -> reversed_tokens(Rest, [], Acc); reversed_tokens([C | Rest], Token, Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, -> reversed_tokens(Rest, [], [Token | Acc]); reversed_tokens([C | Rest], Token, Acc) -> reversed_tokens(Rest, [C | Token], Acc); reversed_tokens(_, _, _) -> undefeined. extract_quoted_string([], _Acc) -> undefined; extract_quoted_string("\"\\" ++ Rest, Acc) -> extract_quoted_string(Rest, "\"" ++ Acc); extract_quoted_string("\"" ++ Rest, Acc) -> {Acc, Rest}; extract_quoted_string([C | Rest], Acc) -> extract_quoted_string(Rest, [C | Acc]). expand({array, L}) -> mochiweb_util:join(lists:reverse(L), ", "); expand(V) -> V. merge("set-cookie", V1, {array, L}) -> {array, [V1 | L]}; merge("set-cookie", V1, V0) -> {array, [V1, V0]}; merge(_, V1, V0) -> V0 ++ ", " ++ V1. normalize(K) when is_list(K) -> string:to_lower(K); normalize(K) when is_atom(K) -> normalize(atom_to_list(K)); normalize(K) when is_binary(K) -> normalize(binary_to_list(K)). any_to_list(V) when is_list(V) -> V; any_to_list(V) when is_atom(V) -> atom_to_list(V); any_to_list(V) when is_binary(V) -> binary_to_list(V); any_to_list(V) when is_integer(V) -> integer_to_list(V). %% %% Tests. %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). make_test() -> Identity = make([{hdr, foo}]), ?assertEqual( Identity, make(Identity)). enter_from_list_test() -> H = make([{hdr, foo}]), ?assertEqual( [{baz, "wibble"}, {hdr, "foo"}], to_list(enter_from_list([{baz, wibble}], H))), ?assertEqual( [{hdr, "bar"}], to_list(enter_from_list([{hdr, bar}], H))), ok. default_from_list_test() -> H = make([{hdr, foo}]), ?assertEqual( [{baz, "wibble"}, {hdr, "foo"}], to_list(default_from_list([{baz, wibble}], H))), ?assertEqual( [{hdr, "foo"}], to_list(default_from_list([{hdr, bar}], H))), ok. get_primary_value_test() -> H = make([{hdr, foo}, {baz, <<"wibble;taco">>}]), ?assertEqual( "foo", get_primary_value(hdr, H)), ?assertEqual( undefined, get_primary_value(bar, H)), ?assertEqual( "wibble", get_primary_value(<<"baz">>, H)), ok. get_combined_value_test() -> H = make([{hdr, foo}, {baz, <<"wibble,taco">>}, {content_length, "123, 123"}, {test, " 123, 123, 123 , 123,123 "}, {test2, "456, 123, 123 , 123"}, {test3, "123"}, {test4, " 123, "}]), ?assertEqual( "foo", get_combined_value(hdr, H)), ?assertEqual( undefined, get_combined_value(bar, H)), ?assertEqual( undefined, get_combined_value(<<"baz">>, H)), ?assertEqual( "123", get_combined_value(<<"content_length">>, H)), ?assertEqual( "123", get_combined_value(<<"test">>, H)), ?assertEqual( undefined, get_combined_value(<<"test2">>, H)), ?assertEqual( "123", get_combined_value(<<"test3">>, H)), ?assertEqual( "123", get_combined_value(<<"test4">>, H)), ok. set_cookie_test() -> H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]), ?assertEqual( [{"set-cookie", "foo"}, {"set-cookie", "bar"}, {"set-cookie", "baz"}], to_list(H)), ok. headers_test() -> H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]), [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H), H1 = ?MODULE:insert(taco, grande, H), [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1), H2 = ?MODULE:make([{"Set-Cookie", "foo"}]), [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2), H3 = ?MODULE:insert("Set-Cookie", "bar", H2), [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3), "foo, bar" = ?MODULE:get_value("set-cookie", H3), {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3), undefined = ?MODULE:get_value("shibby", H3), none = ?MODULE:lookup("shibby", H3), H4 = ?MODULE:insert("content-type", "application/x-www-form-urlencoded; charset=utf8", H3), "application/x-www-form-urlencoded" = ?MODULE:get_primary_value( "content-type", H4), H4 = ?MODULE:delete_any("nonexistent-header", H4), H3 = ?MODULE:delete_any("content-type", H4), HB = <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>, H_HB = ?MODULE:from_binary(HB), H_HB = ?MODULE:from_binary(binary_to_list(HB)), "47" = ?MODULE:get_value("Content-Length", H_HB), "text/plain" = ?MODULE:get_value("Content-Type", H_HB), L_H_HB = ?MODULE:to_list(H_HB), 2 = length(L_H_HB), true = lists:member({'Content-Length', "47"}, L_H_HB), true = lists:member({'Content-Type', "text/plain"}, L_H_HB), HL = [ <<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">> ], HL2 = [ "Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">> ], HL3 = [ <<"Content-Length: 47\r\n">>, "Content-Type: text/plain\r\n" ], H_HL = ?MODULE:from_binary(HL), H_HL = ?MODULE:from_binary(HL2), H_HL = ?MODULE:from_binary(HL3), "47" = ?MODULE:get_value("Content-Length", H_HL), "text/plain" = ?MODULE:get_value("Content-Type", H_HL), L_H_HL = ?MODULE:to_list(H_HL), 2 = length(L_H_HL), true = lists:member({'Content-Length', "47"}, L_H_HL), true = lists:member({'Content-Type', "text/plain"}, L_H_HL), [] = ?MODULE:to_list(?MODULE:from_binary(<<>>)), [] = ?MODULE:to_list(?MODULE:from_binary(<<"">>)), [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n">>)), [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n\r\n">>)), [] = ?MODULE:to_list(?MODULE:from_binary("")), [] = ?MODULE:to_list(?MODULE:from_binary([<<>>])), [] = ?MODULE:to_list(?MODULE:from_binary([<<"">>])), [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n">>])), [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])), ok. tokenize_header_value_test() -> ?assertEqual(["a quote in a \"quote\"."], tokenize_header_value("\"a quote in a \\\"quote\\\".\"")), ?assertEqual(["abc"], tokenize_header_value("abc")), ?assertEqual(["abc", "def"], tokenize_header_value("abc def")), ?assertEqual(["abc", "def"], tokenize_header_value("abc , def")), ?assertEqual(["abc", "def"], tokenize_header_value(",abc ,, def,,")), ?assertEqual(["abc def"], tokenize_header_value("\"abc def\" ")), ?assertEqual(["abc, def"], tokenize_header_value("\"abc, def\"")), ?assertEqual(["\\a\\$"], tokenize_header_value("\"\\a\\$\"")), ?assertEqual(["abc def", "foo, bar", "12345", ""], tokenize_header_value("\"abc def\" \"foo, bar\" , 12345, \"\"")), ?assertEqual(undefined, tokenize_header_value(undefined)), ?assertEqual(undefined, tokenize_header_value("umatched quote\"")), ?assertEqual(undefined, tokenize_header_value("\"unmatched quote")). -endif. tsung-1.8.0/src/lib/mochiweb_charref.erl0000644000201100017670000020623414377756736017735 0ustar nniclausdream%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Converts HTML 5 charrefs and entities to codepoints (or lists of code points). -module(mochiweb_charref). -export([charref/1]). %% External API. %% @doc Convert a decimal charref, hex charref, or html entity to a unicode %% codepoint, or return undefined on failure. %% The input should not include an ampersand or semicolon. %% charref("#38") = 38, charref("#x26") = 38, charref("amp") = 38. -spec charref(binary() | string()) -> integer() | [integer()] | undefined. charref(B) when is_binary(B) -> charref(binary_to_list(B)); charref([$#, C | L]) when C =:= $x orelse C =:= $X -> try erlang:list_to_integer(L, 16) catch error:badarg -> undefined end; charref([$# | L]) -> try list_to_integer(L) catch error:badarg -> undefined end; charref(L) -> entity(L). %% Internal API. %% [2011-10-14] Generated from: %% http://www.w3.org/TR/html5/named-character-references.html entity("AElig") -> 16#000C6; entity("AMP") -> 16#00026; entity("Aacute") -> 16#000C1; entity("Abreve") -> 16#00102; entity("Acirc") -> 16#000C2; entity("Acy") -> 16#00410; entity("Afr") -> 16#1D504; entity("Agrave") -> 16#000C0; entity("Alpha") -> 16#00391; entity("Amacr") -> 16#00100; entity("And") -> 16#02A53; entity("Aogon") -> 16#00104; entity("Aopf") -> 16#1D538; entity("ApplyFunction") -> 16#02061; entity("Aring") -> 16#000C5; entity("Ascr") -> 16#1D49C; entity("Assign") -> 16#02254; entity("Atilde") -> 16#000C3; entity("Auml") -> 16#000C4; entity("Backslash") -> 16#02216; entity("Barv") -> 16#02AE7; entity("Barwed") -> 16#02306; entity("Bcy") -> 16#00411; entity("Because") -> 16#02235; entity("Bernoullis") -> 16#0212C; entity("Beta") -> 16#00392; entity("Bfr") -> 16#1D505; entity("Bopf") -> 16#1D539; entity("Breve") -> 16#002D8; entity("Bscr") -> 16#0212C; entity("Bumpeq") -> 16#0224E; entity("CHcy") -> 16#00427; entity("COPY") -> 16#000A9; entity("Cacute") -> 16#00106; entity("Cap") -> 16#022D2; entity("CapitalDifferentialD") -> 16#02145; entity("Cayleys") -> 16#0212D; entity("Ccaron") -> 16#0010C; entity("Ccedil") -> 16#000C7; entity("Ccirc") -> 16#00108; entity("Cconint") -> 16#02230; entity("Cdot") -> 16#0010A; entity("Cedilla") -> 16#000B8; entity("CenterDot") -> 16#000B7; entity("Cfr") -> 16#0212D; entity("Chi") -> 16#003A7; entity("CircleDot") -> 16#02299; entity("CircleMinus") -> 16#02296; entity("CirclePlus") -> 16#02295; entity("CircleTimes") -> 16#02297; entity("ClockwiseContourIntegral") -> 16#02232; entity("CloseCurlyDoubleQuote") -> 16#0201D; entity("CloseCurlyQuote") -> 16#02019; entity("Colon") -> 16#02237; entity("Colone") -> 16#02A74; entity("Congruent") -> 16#02261; entity("Conint") -> 16#0222F; entity("ContourIntegral") -> 16#0222E; entity("Copf") -> 16#02102; entity("Coproduct") -> 16#02210; entity("CounterClockwiseContourIntegral") -> 16#02233; entity("Cross") -> 16#02A2F; entity("Cscr") -> 16#1D49E; entity("Cup") -> 16#022D3; entity("CupCap") -> 16#0224D; entity("DD") -> 16#02145; entity("DDotrahd") -> 16#02911; entity("DJcy") -> 16#00402; entity("DScy") -> 16#00405; entity("DZcy") -> 16#0040F; entity("Dagger") -> 16#02021; entity("Darr") -> 16#021A1; entity("Dashv") -> 16#02AE4; entity("Dcaron") -> 16#0010E; entity("Dcy") -> 16#00414; entity("Del") -> 16#02207; entity("Delta") -> 16#00394; entity("Dfr") -> 16#1D507; entity("DiacriticalAcute") -> 16#000B4; entity("DiacriticalDot") -> 16#002D9; entity("DiacriticalDoubleAcute") -> 16#002DD; entity("DiacriticalGrave") -> 16#00060; entity("DiacriticalTilde") -> 16#002DC; entity("Diamond") -> 16#022C4; entity("DifferentialD") -> 16#02146; entity("Dopf") -> 16#1D53B; entity("Dot") -> 16#000A8; entity("DotDot") -> 16#020DC; entity("DotEqual") -> 16#02250; entity("DoubleContourIntegral") -> 16#0222F; entity("DoubleDot") -> 16#000A8; entity("DoubleDownArrow") -> 16#021D3; entity("DoubleLeftArrow") -> 16#021D0; entity("DoubleLeftRightArrow") -> 16#021D4; entity("DoubleLeftTee") -> 16#02AE4; entity("DoubleLongLeftArrow") -> 16#027F8; entity("DoubleLongLeftRightArrow") -> 16#027FA; entity("DoubleLongRightArrow") -> 16#027F9; entity("DoubleRightArrow") -> 16#021D2; entity("DoubleRightTee") -> 16#022A8; entity("DoubleUpArrow") -> 16#021D1; entity("DoubleUpDownArrow") -> 16#021D5; entity("DoubleVerticalBar") -> 16#02225; entity("DownArrow") -> 16#02193; entity("DownArrowBar") -> 16#02913; entity("DownArrowUpArrow") -> 16#021F5; entity("DownBreve") -> 16#00311; entity("DownLeftRightVector") -> 16#02950; entity("DownLeftTeeVector") -> 16#0295E; entity("DownLeftVector") -> 16#021BD; entity("DownLeftVectorBar") -> 16#02956; entity("DownRightTeeVector") -> 16#0295F; entity("DownRightVector") -> 16#021C1; entity("DownRightVectorBar") -> 16#02957; entity("DownTee") -> 16#022A4; entity("DownTeeArrow") -> 16#021A7; entity("Downarrow") -> 16#021D3; entity("Dscr") -> 16#1D49F; entity("Dstrok") -> 16#00110; entity("ENG") -> 16#0014A; entity("ETH") -> 16#000D0; entity("Eacute") -> 16#000C9; entity("Ecaron") -> 16#0011A; entity("Ecirc") -> 16#000CA; entity("Ecy") -> 16#0042D; entity("Edot") -> 16#00116; entity("Efr") -> 16#1D508; entity("Egrave") -> 16#000C8; entity("Element") -> 16#02208; entity("Emacr") -> 16#00112; entity("EmptySmallSquare") -> 16#025FB; entity("EmptyVerySmallSquare") -> 16#025AB; entity("Eogon") -> 16#00118; entity("Eopf") -> 16#1D53C; entity("Epsilon") -> 16#00395; entity("Equal") -> 16#02A75; entity("EqualTilde") -> 16#02242; entity("Equilibrium") -> 16#021CC; entity("Escr") -> 16#02130; entity("Esim") -> 16#02A73; entity("Eta") -> 16#00397; entity("Euml") -> 16#000CB; entity("Exists") -> 16#02203; entity("ExponentialE") -> 16#02147; entity("Fcy") -> 16#00424; entity("Ffr") -> 16#1D509; entity("FilledSmallSquare") -> 16#025FC; entity("FilledVerySmallSquare") -> 16#025AA; entity("Fopf") -> 16#1D53D; entity("ForAll") -> 16#02200; entity("Fouriertrf") -> 16#02131; entity("Fscr") -> 16#02131; entity("GJcy") -> 16#00403; entity("GT") -> 16#0003E; entity("Gamma") -> 16#00393; entity("Gammad") -> 16#003DC; entity("Gbreve") -> 16#0011E; entity("Gcedil") -> 16#00122; entity("Gcirc") -> 16#0011C; entity("Gcy") -> 16#00413; entity("Gdot") -> 16#00120; entity("Gfr") -> 16#1D50A; entity("Gg") -> 16#022D9; entity("Gopf") -> 16#1D53E; entity("GreaterEqual") -> 16#02265; entity("GreaterEqualLess") -> 16#022DB; entity("GreaterFullEqual") -> 16#02267; entity("GreaterGreater") -> 16#02AA2; entity("GreaterLess") -> 16#02277; entity("GreaterSlantEqual") -> 16#02A7E; entity("GreaterTilde") -> 16#02273; entity("Gscr") -> 16#1D4A2; entity("Gt") -> 16#0226B; entity("HARDcy") -> 16#0042A; entity("Hacek") -> 16#002C7; entity("Hat") -> 16#0005E; entity("Hcirc") -> 16#00124; entity("Hfr") -> 16#0210C; entity("HilbertSpace") -> 16#0210B; entity("Hopf") -> 16#0210D; entity("HorizontalLine") -> 16#02500; entity("Hscr") -> 16#0210B; entity("Hstrok") -> 16#00126; entity("HumpDownHump") -> 16#0224E; entity("HumpEqual") -> 16#0224F; entity("IEcy") -> 16#00415; entity("IJlig") -> 16#00132; entity("IOcy") -> 16#00401; entity("Iacute") -> 16#000CD; entity("Icirc") -> 16#000CE; entity("Icy") -> 16#00418; entity("Idot") -> 16#00130; entity("Ifr") -> 16#02111; entity("Igrave") -> 16#000CC; entity("Im") -> 16#02111; entity("Imacr") -> 16#0012A; entity("ImaginaryI") -> 16#02148; entity("Implies") -> 16#021D2; entity("Int") -> 16#0222C; entity("Integral") -> 16#0222B; entity("Intersection") -> 16#022C2; entity("InvisibleComma") -> 16#02063; entity("InvisibleTimes") -> 16#02062; entity("Iogon") -> 16#0012E; entity("Iopf") -> 16#1D540; entity("Iota") -> 16#00399; entity("Iscr") -> 16#02110; entity("Itilde") -> 16#00128; entity("Iukcy") -> 16#00406; entity("Iuml") -> 16#000CF; entity("Jcirc") -> 16#00134; entity("Jcy") -> 16#00419; entity("Jfr") -> 16#1D50D; entity("Jopf") -> 16#1D541; entity("Jscr") -> 16#1D4A5; entity("Jsercy") -> 16#00408; entity("Jukcy") -> 16#00404; entity("KHcy") -> 16#00425; entity("KJcy") -> 16#0040C; entity("Kappa") -> 16#0039A; entity("Kcedil") -> 16#00136; entity("Kcy") -> 16#0041A; entity("Kfr") -> 16#1D50E; entity("Kopf") -> 16#1D542; entity("Kscr") -> 16#1D4A6; entity("LJcy") -> 16#00409; entity("LT") -> 16#0003C; entity("Lacute") -> 16#00139; entity("Lambda") -> 16#0039B; entity("Lang") -> 16#027EA; entity("Laplacetrf") -> 16#02112; entity("Larr") -> 16#0219E; entity("Lcaron") -> 16#0013D; entity("Lcedil") -> 16#0013B; entity("Lcy") -> 16#0041B; entity("LeftAngleBracket") -> 16#027E8; entity("LeftArrow") -> 16#02190; entity("LeftArrowBar") -> 16#021E4; entity("LeftArrowRightArrow") -> 16#021C6; entity("LeftCeiling") -> 16#02308; entity("LeftDoubleBracket") -> 16#027E6; entity("LeftDownTeeVector") -> 16#02961; entity("LeftDownVector") -> 16#021C3; entity("LeftDownVectorBar") -> 16#02959; entity("LeftFloor") -> 16#0230A; entity("LeftRightArrow") -> 16#02194; entity("LeftRightVector") -> 16#0294E; entity("LeftTee") -> 16#022A3; entity("LeftTeeArrow") -> 16#021A4; entity("LeftTeeVector") -> 16#0295A; entity("LeftTriangle") -> 16#022B2; entity("LeftTriangleBar") -> 16#029CF; entity("LeftTriangleEqual") -> 16#022B4; entity("LeftUpDownVector") -> 16#02951; entity("LeftUpTeeVector") -> 16#02960; entity("LeftUpVector") -> 16#021BF; entity("LeftUpVectorBar") -> 16#02958; entity("LeftVector") -> 16#021BC; entity("LeftVectorBar") -> 16#02952; entity("Leftarrow") -> 16#021D0; entity("Leftrightarrow") -> 16#021D4; entity("LessEqualGreater") -> 16#022DA; entity("LessFullEqual") -> 16#02266; entity("LessGreater") -> 16#02276; entity("LessLess") -> 16#02AA1; entity("LessSlantEqual") -> 16#02A7D; entity("LessTilde") -> 16#02272; entity("Lfr") -> 16#1D50F; entity("Ll") -> 16#022D8; entity("Lleftarrow") -> 16#021DA; entity("Lmidot") -> 16#0013F; entity("LongLeftArrow") -> 16#027F5; entity("LongLeftRightArrow") -> 16#027F7; entity("LongRightArrow") -> 16#027F6; entity("Longleftarrow") -> 16#027F8; entity("Longleftrightarrow") -> 16#027FA; entity("Longrightarrow") -> 16#027F9; entity("Lopf") -> 16#1D543; entity("LowerLeftArrow") -> 16#02199; entity("LowerRightArrow") -> 16#02198; entity("Lscr") -> 16#02112; entity("Lsh") -> 16#021B0; entity("Lstrok") -> 16#00141; entity("Lt") -> 16#0226A; entity("Map") -> 16#02905; entity("Mcy") -> 16#0041C; entity("MediumSpace") -> 16#0205F; entity("Mellintrf") -> 16#02133; entity("Mfr") -> 16#1D510; entity("MinusPlus") -> 16#02213; entity("Mopf") -> 16#1D544; entity("Mscr") -> 16#02133; entity("Mu") -> 16#0039C; entity("NJcy") -> 16#0040A; entity("Nacute") -> 16#00143; entity("Ncaron") -> 16#00147; entity("Ncedil") -> 16#00145; entity("Ncy") -> 16#0041D; entity("NegativeMediumSpace") -> 16#0200B; entity("NegativeThickSpace") -> 16#0200B; entity("NegativeThinSpace") -> 16#0200B; entity("NegativeVeryThinSpace") -> 16#0200B; entity("NestedGreaterGreater") -> 16#0226B; entity("NestedLessLess") -> 16#0226A; entity("NewLine") -> 16#0000A; entity("Nfr") -> 16#1D511; entity("NoBreak") -> 16#02060; entity("NonBreakingSpace") -> 16#000A0; entity("Nopf") -> 16#02115; entity("Not") -> 16#02AEC; entity("NotCongruent") -> 16#02262; entity("NotCupCap") -> 16#0226D; entity("NotDoubleVerticalBar") -> 16#02226; entity("NotElement") -> 16#02209; entity("NotEqual") -> 16#02260; entity("NotEqualTilde") -> [16#02242, 16#00338]; entity("NotExists") -> 16#02204; entity("NotGreater") -> 16#0226F; entity("NotGreaterEqual") -> 16#02271; entity("NotGreaterFullEqual") -> [16#02267, 16#00338]; entity("NotGreaterGreater") -> [16#0226B, 16#00338]; entity("NotGreaterLess") -> 16#02279; entity("NotGreaterSlantEqual") -> [16#02A7E, 16#00338]; entity("NotGreaterTilde") -> 16#02275; entity("NotHumpDownHump") -> [16#0224E, 16#00338]; entity("NotHumpEqual") -> [16#0224F, 16#00338]; entity("NotLeftTriangle") -> 16#022EA; entity("NotLeftTriangleBar") -> [16#029CF, 16#00338]; entity("NotLeftTriangleEqual") -> 16#022EC; entity("NotLess") -> 16#0226E; entity("NotLessEqual") -> 16#02270; entity("NotLessGreater") -> 16#02278; entity("NotLessLess") -> [16#0226A, 16#00338]; entity("NotLessSlantEqual") -> [16#02A7D, 16#00338]; entity("NotLessTilde") -> 16#02274; entity("NotNestedGreaterGreater") -> [16#02AA2, 16#00338]; entity("NotNestedLessLess") -> [16#02AA1, 16#00338]; entity("NotPrecedes") -> 16#02280; entity("NotPrecedesEqual") -> [16#02AAF, 16#00338]; entity("NotPrecedesSlantEqual") -> 16#022E0; entity("NotReverseElement") -> 16#0220C; entity("NotRightTriangle") -> 16#022EB; entity("NotRightTriangleBar") -> [16#029D0, 16#00338]; entity("NotRightTriangleEqual") -> 16#022ED; entity("NotSquareSubset") -> [16#0228F, 16#00338]; entity("NotSquareSubsetEqual") -> 16#022E2; entity("NotSquareSuperset") -> [16#02290, 16#00338]; entity("NotSquareSupersetEqual") -> 16#022E3; entity("NotSubset") -> [16#02282, 16#020D2]; entity("NotSubsetEqual") -> 16#02288; entity("NotSucceeds") -> 16#02281; entity("NotSucceedsEqual") -> [16#02AB0, 16#00338]; entity("NotSucceedsSlantEqual") -> 16#022E1; entity("NotSucceedsTilde") -> [16#0227F, 16#00338]; entity("NotSuperset") -> [16#02283, 16#020D2]; entity("NotSupersetEqual") -> 16#02289; entity("NotTilde") -> 16#02241; entity("NotTildeEqual") -> 16#02244; entity("NotTildeFullEqual") -> 16#02247; entity("NotTildeTilde") -> 16#02249; entity("NotVerticalBar") -> 16#02224; entity("Nscr") -> 16#1D4A9; entity("Ntilde") -> 16#000D1; entity("Nu") -> 16#0039D; entity("OElig") -> 16#00152; entity("Oacute") -> 16#000D3; entity("Ocirc") -> 16#000D4; entity("Ocy") -> 16#0041E; entity("Odblac") -> 16#00150; entity("Ofr") -> 16#1D512; entity("Ograve") -> 16#000D2; entity("Omacr") -> 16#0014C; entity("Omega") -> 16#003A9; entity("Omicron") -> 16#0039F; entity("Oopf") -> 16#1D546; entity("OpenCurlyDoubleQuote") -> 16#0201C; entity("OpenCurlyQuote") -> 16#02018; entity("Or") -> 16#02A54; entity("Oscr") -> 16#1D4AA; entity("Oslash") -> 16#000D8; entity("Otilde") -> 16#000D5; entity("Otimes") -> 16#02A37; entity("Ouml") -> 16#000D6; entity("OverBar") -> 16#0203E; entity("OverBrace") -> 16#023DE; entity("OverBracket") -> 16#023B4; entity("OverParenthesis") -> 16#023DC; entity("PartialD") -> 16#02202; entity("Pcy") -> 16#0041F; entity("Pfr") -> 16#1D513; entity("Phi") -> 16#003A6; entity("Pi") -> 16#003A0; entity("PlusMinus") -> 16#000B1; entity("Poincareplane") -> 16#0210C; entity("Popf") -> 16#02119; entity("Pr") -> 16#02ABB; entity("Precedes") -> 16#0227A; entity("PrecedesEqual") -> 16#02AAF; entity("PrecedesSlantEqual") -> 16#0227C; entity("PrecedesTilde") -> 16#0227E; entity("Prime") -> 16#02033; entity("Product") -> 16#0220F; entity("Proportion") -> 16#02237; entity("Proportional") -> 16#0221D; entity("Pscr") -> 16#1D4AB; entity("Psi") -> 16#003A8; entity("QUOT") -> 16#00022; entity("Qfr") -> 16#1D514; entity("Qopf") -> 16#0211A; entity("Qscr") -> 16#1D4AC; entity("RBarr") -> 16#02910; entity("REG") -> 16#000AE; entity("Racute") -> 16#00154; entity("Rang") -> 16#027EB; entity("Rarr") -> 16#021A0; entity("Rarrtl") -> 16#02916; entity("Rcaron") -> 16#00158; entity("Rcedil") -> 16#00156; entity("Rcy") -> 16#00420; entity("Re") -> 16#0211C; entity("ReverseElement") -> 16#0220B; entity("ReverseEquilibrium") -> 16#021CB; entity("ReverseUpEquilibrium") -> 16#0296F; entity("Rfr") -> 16#0211C; entity("Rho") -> 16#003A1; entity("RightAngleBracket") -> 16#027E9; entity("RightArrow") -> 16#02192; entity("RightArrowBar") -> 16#021E5; entity("RightArrowLeftArrow") -> 16#021C4; entity("RightCeiling") -> 16#02309; entity("RightDoubleBracket") -> 16#027E7; entity("RightDownTeeVector") -> 16#0295D; entity("RightDownVector") -> 16#021C2; entity("RightDownVectorBar") -> 16#02955; entity("RightFloor") -> 16#0230B; entity("RightTee") -> 16#022A2; entity("RightTeeArrow") -> 16#021A6; entity("RightTeeVector") -> 16#0295B; entity("RightTriangle") -> 16#022B3; entity("RightTriangleBar") -> 16#029D0; entity("RightTriangleEqual") -> 16#022B5; entity("RightUpDownVector") -> 16#0294F; entity("RightUpTeeVector") -> 16#0295C; entity("RightUpVector") -> 16#021BE; entity("RightUpVectorBar") -> 16#02954; entity("RightVector") -> 16#021C0; entity("RightVectorBar") -> 16#02953; entity("Rightarrow") -> 16#021D2; entity("Ropf") -> 16#0211D; entity("RoundImplies") -> 16#02970; entity("Rrightarrow") -> 16#021DB; entity("Rscr") -> 16#0211B; entity("Rsh") -> 16#021B1; entity("RuleDelayed") -> 16#029F4; entity("SHCHcy") -> 16#00429; entity("SHcy") -> 16#00428; entity("SOFTcy") -> 16#0042C; entity("Sacute") -> 16#0015A; entity("Sc") -> 16#02ABC; entity("Scaron") -> 16#00160; entity("Scedil") -> 16#0015E; entity("Scirc") -> 16#0015C; entity("Scy") -> 16#00421; entity("Sfr") -> 16#1D516; entity("ShortDownArrow") -> 16#02193; entity("ShortLeftArrow") -> 16#02190; entity("ShortRightArrow") -> 16#02192; entity("ShortUpArrow") -> 16#02191; entity("Sigma") -> 16#003A3; entity("SmallCircle") -> 16#02218; entity("Sopf") -> 16#1D54A; entity("Sqrt") -> 16#0221A; entity("Square") -> 16#025A1; entity("SquareIntersection") -> 16#02293; entity("SquareSubset") -> 16#0228F; entity("SquareSubsetEqual") -> 16#02291; entity("SquareSuperset") -> 16#02290; entity("SquareSupersetEqual") -> 16#02292; entity("SquareUnion") -> 16#02294; entity("Sscr") -> 16#1D4AE; entity("Star") -> 16#022C6; entity("Sub") -> 16#022D0; entity("Subset") -> 16#022D0; entity("SubsetEqual") -> 16#02286; entity("Succeeds") -> 16#0227B; entity("SucceedsEqual") -> 16#02AB0; entity("SucceedsSlantEqual") -> 16#0227D; entity("SucceedsTilde") -> 16#0227F; entity("SuchThat") -> 16#0220B; entity("Sum") -> 16#02211; entity("Sup") -> 16#022D1; entity("Superset") -> 16#02283; entity("SupersetEqual") -> 16#02287; entity("Supset") -> 16#022D1; entity("THORN") -> 16#000DE; entity("TRADE") -> 16#02122; entity("TSHcy") -> 16#0040B; entity("TScy") -> 16#00426; entity("Tab") -> 16#00009; entity("Tau") -> 16#003A4; entity("Tcaron") -> 16#00164; entity("Tcedil") -> 16#00162; entity("Tcy") -> 16#00422; entity("Tfr") -> 16#1D517; entity("Therefore") -> 16#02234; entity("Theta") -> 16#00398; entity("ThickSpace") -> [16#0205F, 16#0200A]; entity("ThinSpace") -> 16#02009; entity("Tilde") -> 16#0223C; entity("TildeEqual") -> 16#02243; entity("TildeFullEqual") -> 16#02245; entity("TildeTilde") -> 16#02248; entity("Topf") -> 16#1D54B; entity("TripleDot") -> 16#020DB; entity("Tscr") -> 16#1D4AF; entity("Tstrok") -> 16#00166; entity("Uacute") -> 16#000DA; entity("Uarr") -> 16#0219F; entity("Uarrocir") -> 16#02949; entity("Ubrcy") -> 16#0040E; entity("Ubreve") -> 16#0016C; entity("Ucirc") -> 16#000DB; entity("Ucy") -> 16#00423; entity("Udblac") -> 16#00170; entity("Ufr") -> 16#1D518; entity("Ugrave") -> 16#000D9; entity("Umacr") -> 16#0016A; entity("UnderBar") -> 16#0005F; entity("UnderBrace") -> 16#023DF; entity("UnderBracket") -> 16#023B5; entity("UnderParenthesis") -> 16#023DD; entity("Union") -> 16#022C3; entity("UnionPlus") -> 16#0228E; entity("Uogon") -> 16#00172; entity("Uopf") -> 16#1D54C; entity("UpArrow") -> 16#02191; entity("UpArrowBar") -> 16#02912; entity("UpArrowDownArrow") -> 16#021C5; entity("UpDownArrow") -> 16#02195; entity("UpEquilibrium") -> 16#0296E; entity("UpTee") -> 16#022A5; entity("UpTeeArrow") -> 16#021A5; entity("Uparrow") -> 16#021D1; entity("Updownarrow") -> 16#021D5; entity("UpperLeftArrow") -> 16#02196; entity("UpperRightArrow") -> 16#02197; entity("Upsi") -> 16#003D2; entity("Upsilon") -> 16#003A5; entity("Uring") -> 16#0016E; entity("Uscr") -> 16#1D4B0; entity("Utilde") -> 16#00168; entity("Uuml") -> 16#000DC; entity("VDash") -> 16#022AB; entity("Vbar") -> 16#02AEB; entity("Vcy") -> 16#00412; entity("Vdash") -> 16#022A9; entity("Vdashl") -> 16#02AE6; entity("Vee") -> 16#022C1; entity("Verbar") -> 16#02016; entity("Vert") -> 16#02016; entity("VerticalBar") -> 16#02223; entity("VerticalLine") -> 16#0007C; entity("VerticalSeparator") -> 16#02758; entity("VerticalTilde") -> 16#02240; entity("VeryThinSpace") -> 16#0200A; entity("Vfr") -> 16#1D519; entity("Vopf") -> 16#1D54D; entity("Vscr") -> 16#1D4B1; entity("Vvdash") -> 16#022AA; entity("Wcirc") -> 16#00174; entity("Wedge") -> 16#022C0; entity("Wfr") -> 16#1D51A; entity("Wopf") -> 16#1D54E; entity("Wscr") -> 16#1D4B2; entity("Xfr") -> 16#1D51B; entity("Xi") -> 16#0039E; entity("Xopf") -> 16#1D54F; entity("Xscr") -> 16#1D4B3; entity("YAcy") -> 16#0042F; entity("YIcy") -> 16#00407; entity("YUcy") -> 16#0042E; entity("Yacute") -> 16#000DD; entity("Ycirc") -> 16#00176; entity("Ycy") -> 16#0042B; entity("Yfr") -> 16#1D51C; entity("Yopf") -> 16#1D550; entity("Yscr") -> 16#1D4B4; entity("Yuml") -> 16#00178; entity("ZHcy") -> 16#00416; entity("Zacute") -> 16#00179; entity("Zcaron") -> 16#0017D; entity("Zcy") -> 16#00417; entity("Zdot") -> 16#0017B; entity("ZeroWidthSpace") -> 16#0200B; entity("Zeta") -> 16#00396; entity("Zfr") -> 16#02128; entity("Zopf") -> 16#02124; entity("Zscr") -> 16#1D4B5; entity("aacute") -> 16#000E1; entity("abreve") -> 16#00103; entity("ac") -> 16#0223E; entity("acE") -> [16#0223E, 16#00333]; entity("acd") -> 16#0223F; entity("acirc") -> 16#000E2; entity("acute") -> 16#000B4; entity("acy") -> 16#00430; entity("aelig") -> 16#000E6; entity("af") -> 16#02061; entity("afr") -> 16#1D51E; entity("agrave") -> 16#000E0; entity("alefsym") -> 16#02135; entity("aleph") -> 16#02135; entity("alpha") -> 16#003B1; entity("amacr") -> 16#00101; entity("amalg") -> 16#02A3F; entity("amp") -> 16#00026; entity("and") -> 16#02227; entity("andand") -> 16#02A55; entity("andd") -> 16#02A5C; entity("andslope") -> 16#02A58; entity("andv") -> 16#02A5A; entity("ang") -> 16#02220; entity("ange") -> 16#029A4; entity("angle") -> 16#02220; entity("angmsd") -> 16#02221; entity("angmsdaa") -> 16#029A8; entity("angmsdab") -> 16#029A9; entity("angmsdac") -> 16#029AA; entity("angmsdad") -> 16#029AB; entity("angmsdae") -> 16#029AC; entity("angmsdaf") -> 16#029AD; entity("angmsdag") -> 16#029AE; entity("angmsdah") -> 16#029AF; entity("angrt") -> 16#0221F; entity("angrtvb") -> 16#022BE; entity("angrtvbd") -> 16#0299D; entity("angsph") -> 16#02222; entity("angst") -> 16#000C5; entity("angzarr") -> 16#0237C; entity("aogon") -> 16#00105; entity("aopf") -> 16#1D552; entity("ap") -> 16#02248; entity("apE") -> 16#02A70; entity("apacir") -> 16#02A6F; entity("ape") -> 16#0224A; entity("apid") -> 16#0224B; entity("apos") -> 16#00027; entity("approx") -> 16#02248; entity("approxeq") -> 16#0224A; entity("aring") -> 16#000E5; entity("ascr") -> 16#1D4B6; entity("ast") -> 16#0002A; entity("asymp") -> 16#02248; entity("asympeq") -> 16#0224D; entity("atilde") -> 16#000E3; entity("auml") -> 16#000E4; entity("awconint") -> 16#02233; entity("awint") -> 16#02A11; entity("bNot") -> 16#02AED; entity("backcong") -> 16#0224C; entity("backepsilon") -> 16#003F6; entity("backprime") -> 16#02035; entity("backsim") -> 16#0223D; entity("backsimeq") -> 16#022CD; entity("barvee") -> 16#022BD; entity("barwed") -> 16#02305; entity("barwedge") -> 16#02305; entity("bbrk") -> 16#023B5; entity("bbrktbrk") -> 16#023B6; entity("bcong") -> 16#0224C; entity("bcy") -> 16#00431; entity("bdquo") -> 16#0201E; entity("becaus") -> 16#02235; entity("because") -> 16#02235; entity("bemptyv") -> 16#029B0; entity("bepsi") -> 16#003F6; entity("bernou") -> 16#0212C; entity("beta") -> 16#003B2; entity("beth") -> 16#02136; entity("between") -> 16#0226C; entity("bfr") -> 16#1D51F; entity("bigcap") -> 16#022C2; entity("bigcirc") -> 16#025EF; entity("bigcup") -> 16#022C3; entity("bigodot") -> 16#02A00; entity("bigoplus") -> 16#02A01; entity("bigotimes") -> 16#02A02; entity("bigsqcup") -> 16#02A06; entity("bigstar") -> 16#02605; entity("bigtriangledown") -> 16#025BD; entity("bigtriangleup") -> 16#025B3; entity("biguplus") -> 16#02A04; entity("bigvee") -> 16#022C1; entity("bigwedge") -> 16#022C0; entity("bkarow") -> 16#0290D; entity("blacklozenge") -> 16#029EB; entity("blacksquare") -> 16#025AA; entity("blacktriangle") -> 16#025B4; entity("blacktriangledown") -> 16#025BE; entity("blacktriangleleft") -> 16#025C2; entity("blacktriangleright") -> 16#025B8; entity("blank") -> 16#02423; entity("blk12") -> 16#02592; entity("blk14") -> 16#02591; entity("blk34") -> 16#02593; entity("block") -> 16#02588; entity("bne") -> [16#0003D, 16#020E5]; entity("bnequiv") -> [16#02261, 16#020E5]; entity("bnot") -> 16#02310; entity("bopf") -> 16#1D553; entity("bot") -> 16#022A5; entity("bottom") -> 16#022A5; entity("bowtie") -> 16#022C8; entity("boxDL") -> 16#02557; entity("boxDR") -> 16#02554; entity("boxDl") -> 16#02556; entity("boxDr") -> 16#02553; entity("boxH") -> 16#02550; entity("boxHD") -> 16#02566; entity("boxHU") -> 16#02569; entity("boxHd") -> 16#02564; entity("boxHu") -> 16#02567; entity("boxUL") -> 16#0255D; entity("boxUR") -> 16#0255A; entity("boxUl") -> 16#0255C; entity("boxUr") -> 16#02559; entity("boxV") -> 16#02551; entity("boxVH") -> 16#0256C; entity("boxVL") -> 16#02563; entity("boxVR") -> 16#02560; entity("boxVh") -> 16#0256B; entity("boxVl") -> 16#02562; entity("boxVr") -> 16#0255F; entity("boxbox") -> 16#029C9; entity("boxdL") -> 16#02555; entity("boxdR") -> 16#02552; entity("boxdl") -> 16#02510; entity("boxdr") -> 16#0250C; entity("boxh") -> 16#02500; entity("boxhD") -> 16#02565; entity("boxhU") -> 16#02568; entity("boxhd") -> 16#0252C; entity("boxhu") -> 16#02534; entity("boxminus") -> 16#0229F; entity("boxplus") -> 16#0229E; entity("boxtimes") -> 16#022A0; entity("boxuL") -> 16#0255B; entity("boxuR") -> 16#02558; entity("boxul") -> 16#02518; entity("boxur") -> 16#02514; entity("boxv") -> 16#02502; entity("boxvH") -> 16#0256A; entity("boxvL") -> 16#02561; entity("boxvR") -> 16#0255E; entity("boxvh") -> 16#0253C; entity("boxvl") -> 16#02524; entity("boxvr") -> 16#0251C; entity("bprime") -> 16#02035; entity("breve") -> 16#002D8; entity("brvbar") -> 16#000A6; entity("bscr") -> 16#1D4B7; entity("bsemi") -> 16#0204F; entity("bsim") -> 16#0223D; entity("bsime") -> 16#022CD; entity("bsol") -> 16#0005C; entity("bsolb") -> 16#029C5; entity("bsolhsub") -> 16#027C8; entity("bull") -> 16#02022; entity("bullet") -> 16#02022; entity("bump") -> 16#0224E; entity("bumpE") -> 16#02AAE; entity("bumpe") -> 16#0224F; entity("bumpeq") -> 16#0224F; entity("cacute") -> 16#00107; entity("cap") -> 16#02229; entity("capand") -> 16#02A44; entity("capbrcup") -> 16#02A49; entity("capcap") -> 16#02A4B; entity("capcup") -> 16#02A47; entity("capdot") -> 16#02A40; entity("caps") -> [16#02229, 16#0FE00]; entity("caret") -> 16#02041; entity("caron") -> 16#002C7; entity("ccaps") -> 16#02A4D; entity("ccaron") -> 16#0010D; entity("ccedil") -> 16#000E7; entity("ccirc") -> 16#00109; entity("ccups") -> 16#02A4C; entity("ccupssm") -> 16#02A50; entity("cdot") -> 16#0010B; entity("cedil") -> 16#000B8; entity("cemptyv") -> 16#029B2; entity("cent") -> 16#000A2; entity("centerdot") -> 16#000B7; entity("cfr") -> 16#1D520; entity("chcy") -> 16#00447; entity("check") -> 16#02713; entity("checkmark") -> 16#02713; entity("chi") -> 16#003C7; entity("cir") -> 16#025CB; entity("cirE") -> 16#029C3; entity("circ") -> 16#002C6; entity("circeq") -> 16#02257; entity("circlearrowleft") -> 16#021BA; entity("circlearrowright") -> 16#021BB; entity("circledR") -> 16#000AE; entity("circledS") -> 16#024C8; entity("circledast") -> 16#0229B; entity("circledcirc") -> 16#0229A; entity("circleddash") -> 16#0229D; entity("cire") -> 16#02257; entity("cirfnint") -> 16#02A10; entity("cirmid") -> 16#02AEF; entity("cirscir") -> 16#029C2; entity("clubs") -> 16#02663; entity("clubsuit") -> 16#02663; entity("colon") -> 16#0003A; entity("colone") -> 16#02254; entity("coloneq") -> 16#02254; entity("comma") -> 16#0002C; entity("commat") -> 16#00040; entity("comp") -> 16#02201; entity("compfn") -> 16#02218; entity("complement") -> 16#02201; entity("complexes") -> 16#02102; entity("cong") -> 16#02245; entity("congdot") -> 16#02A6D; entity("conint") -> 16#0222E; entity("copf") -> 16#1D554; entity("coprod") -> 16#02210; entity("copy") -> 16#000A9; entity("copysr") -> 16#02117; entity("crarr") -> 16#021B5; entity("cross") -> 16#02717; entity("cscr") -> 16#1D4B8; entity("csub") -> 16#02ACF; entity("csube") -> 16#02AD1; entity("csup") -> 16#02AD0; entity("csupe") -> 16#02AD2; entity("ctdot") -> 16#022EF; entity("cudarrl") -> 16#02938; entity("cudarrr") -> 16#02935; entity("cuepr") -> 16#022DE; entity("cuesc") -> 16#022DF; entity("cularr") -> 16#021B6; entity("cularrp") -> 16#0293D; entity("cup") -> 16#0222A; entity("cupbrcap") -> 16#02A48; entity("cupcap") -> 16#02A46; entity("cupcup") -> 16#02A4A; entity("cupdot") -> 16#0228D; entity("cupor") -> 16#02A45; entity("cups") -> [16#0222A, 16#0FE00]; entity("curarr") -> 16#021B7; entity("curarrm") -> 16#0293C; entity("curlyeqprec") -> 16#022DE; entity("curlyeqsucc") -> 16#022DF; entity("curlyvee") -> 16#022CE; entity("curlywedge") -> 16#022CF; entity("curren") -> 16#000A4; entity("curvearrowleft") -> 16#021B6; entity("curvearrowright") -> 16#021B7; entity("cuvee") -> 16#022CE; entity("cuwed") -> 16#022CF; entity("cwconint") -> 16#02232; entity("cwint") -> 16#02231; entity("cylcty") -> 16#0232D; entity("dArr") -> 16#021D3; entity("dHar") -> 16#02965; entity("dagger") -> 16#02020; entity("daleth") -> 16#02138; entity("darr") -> 16#02193; entity("dash") -> 16#02010; entity("dashv") -> 16#022A3; entity("dbkarow") -> 16#0290F; entity("dblac") -> 16#002DD; entity("dcaron") -> 16#0010F; entity("dcy") -> 16#00434; entity("dd") -> 16#02146; entity("ddagger") -> 16#02021; entity("ddarr") -> 16#021CA; entity("ddotseq") -> 16#02A77; entity("deg") -> 16#000B0; entity("delta") -> 16#003B4; entity("demptyv") -> 16#029B1; entity("dfisht") -> 16#0297F; entity("dfr") -> 16#1D521; entity("dharl") -> 16#021C3; entity("dharr") -> 16#021C2; entity("diam") -> 16#022C4; entity("diamond") -> 16#022C4; entity("diamondsuit") -> 16#02666; entity("diams") -> 16#02666; entity("die") -> 16#000A8; entity("digamma") -> 16#003DD; entity("disin") -> 16#022F2; entity("div") -> 16#000F7; entity("divide") -> 16#000F7; entity("divideontimes") -> 16#022C7; entity("divonx") -> 16#022C7; entity("djcy") -> 16#00452; entity("dlcorn") -> 16#0231E; entity("dlcrop") -> 16#0230D; entity("dollar") -> 16#00024; entity("dopf") -> 16#1D555; entity("dot") -> 16#002D9; entity("doteq") -> 16#02250; entity("doteqdot") -> 16#02251; entity("dotminus") -> 16#02238; entity("dotplus") -> 16#02214; entity("dotsquare") -> 16#022A1; entity("doublebarwedge") -> 16#02306; entity("downarrow") -> 16#02193; entity("downdownarrows") -> 16#021CA; entity("downharpoonleft") -> 16#021C3; entity("downharpoonright") -> 16#021C2; entity("drbkarow") -> 16#02910; entity("drcorn") -> 16#0231F; entity("drcrop") -> 16#0230C; entity("dscr") -> 16#1D4B9; entity("dscy") -> 16#00455; entity("dsol") -> 16#029F6; entity("dstrok") -> 16#00111; entity("dtdot") -> 16#022F1; entity("dtri") -> 16#025BF; entity("dtrif") -> 16#025BE; entity("duarr") -> 16#021F5; entity("duhar") -> 16#0296F; entity("dwangle") -> 16#029A6; entity("dzcy") -> 16#0045F; entity("dzigrarr") -> 16#027FF; entity("eDDot") -> 16#02A77; entity("eDot") -> 16#02251; entity("eacute") -> 16#000E9; entity("easter") -> 16#02A6E; entity("ecaron") -> 16#0011B; entity("ecir") -> 16#02256; entity("ecirc") -> 16#000EA; entity("ecolon") -> 16#02255; entity("ecy") -> 16#0044D; entity("edot") -> 16#00117; entity("ee") -> 16#02147; entity("efDot") -> 16#02252; entity("efr") -> 16#1D522; entity("eg") -> 16#02A9A; entity("egrave") -> 16#000E8; entity("egs") -> 16#02A96; entity("egsdot") -> 16#02A98; entity("el") -> 16#02A99; entity("elinters") -> 16#023E7; entity("ell") -> 16#02113; entity("els") -> 16#02A95; entity("elsdot") -> 16#02A97; entity("emacr") -> 16#00113; entity("empty") -> 16#02205; entity("emptyset") -> 16#02205; entity("emptyv") -> 16#02205; entity("emsp") -> 16#02003; entity("emsp13") -> 16#02004; entity("emsp14") -> 16#02005; entity("eng") -> 16#0014B; entity("ensp") -> 16#02002; entity("eogon") -> 16#00119; entity("eopf") -> 16#1D556; entity("epar") -> 16#022D5; entity("eparsl") -> 16#029E3; entity("eplus") -> 16#02A71; entity("epsi") -> 16#003B5; entity("epsilon") -> 16#003B5; entity("epsiv") -> 16#003F5; entity("eqcirc") -> 16#02256; entity("eqcolon") -> 16#02255; entity("eqsim") -> 16#02242; entity("eqslantgtr") -> 16#02A96; entity("eqslantless") -> 16#02A95; entity("equals") -> 16#0003D; entity("equest") -> 16#0225F; entity("equiv") -> 16#02261; entity("equivDD") -> 16#02A78; entity("eqvparsl") -> 16#029E5; entity("erDot") -> 16#02253; entity("erarr") -> 16#02971; entity("escr") -> 16#0212F; entity("esdot") -> 16#02250; entity("esim") -> 16#02242; entity("eta") -> 16#003B7; entity("eth") -> 16#000F0; entity("euml") -> 16#000EB; entity("euro") -> 16#020AC; entity("excl") -> 16#00021; entity("exist") -> 16#02203; entity("expectation") -> 16#02130; entity("exponentiale") -> 16#02147; entity("fallingdotseq") -> 16#02252; entity("fcy") -> 16#00444; entity("female") -> 16#02640; entity("ffilig") -> 16#0FB03; entity("fflig") -> 16#0FB00; entity("ffllig") -> 16#0FB04; entity("ffr") -> 16#1D523; entity("filig") -> 16#0FB01; entity("fjlig") -> [16#00066, 16#0006A]; entity("flat") -> 16#0266D; entity("fllig") -> 16#0FB02; entity("fltns") -> 16#025B1; entity("fnof") -> 16#00192; entity("fopf") -> 16#1D557; entity("forall") -> 16#02200; entity("fork") -> 16#022D4; entity("forkv") -> 16#02AD9; entity("fpartint") -> 16#02A0D; entity("frac12") -> 16#000BD; entity("frac13") -> 16#02153; entity("frac14") -> 16#000BC; entity("frac15") -> 16#02155; entity("frac16") -> 16#02159; entity("frac18") -> 16#0215B; entity("frac23") -> 16#02154; entity("frac25") -> 16#02156; entity("frac34") -> 16#000BE; entity("frac35") -> 16#02157; entity("frac38") -> 16#0215C; entity("frac45") -> 16#02158; entity("frac56") -> 16#0215A; entity("frac58") -> 16#0215D; entity("frac78") -> 16#0215E; entity("frasl") -> 16#02044; entity("frown") -> 16#02322; entity("fscr") -> 16#1D4BB; entity("gE") -> 16#02267; entity("gEl") -> 16#02A8C; entity("gacute") -> 16#001F5; entity("gamma") -> 16#003B3; entity("gammad") -> 16#003DD; entity("gap") -> 16#02A86; entity("gbreve") -> 16#0011F; entity("gcirc") -> 16#0011D; entity("gcy") -> 16#00433; entity("gdot") -> 16#00121; entity("ge") -> 16#02265; entity("gel") -> 16#022DB; entity("geq") -> 16#02265; entity("geqq") -> 16#02267; entity("geqslant") -> 16#02A7E; entity("ges") -> 16#02A7E; entity("gescc") -> 16#02AA9; entity("gesdot") -> 16#02A80; entity("gesdoto") -> 16#02A82; entity("gesdotol") -> 16#02A84; entity("gesl") -> [16#022DB, 16#0FE00]; entity("gesles") -> 16#02A94; entity("gfr") -> 16#1D524; entity("gg") -> 16#0226B; entity("ggg") -> 16#022D9; entity("gimel") -> 16#02137; entity("gjcy") -> 16#00453; entity("gl") -> 16#02277; entity("glE") -> 16#02A92; entity("gla") -> 16#02AA5; entity("glj") -> 16#02AA4; entity("gnE") -> 16#02269; entity("gnap") -> 16#02A8A; entity("gnapprox") -> 16#02A8A; entity("gne") -> 16#02A88; entity("gneq") -> 16#02A88; entity("gneqq") -> 16#02269; entity("gnsim") -> 16#022E7; entity("gopf") -> 16#1D558; entity("grave") -> 16#00060; entity("gscr") -> 16#0210A; entity("gsim") -> 16#02273; entity("gsime") -> 16#02A8E; entity("gsiml") -> 16#02A90; entity("gt") -> 16#0003E; entity("gtcc") -> 16#02AA7; entity("gtcir") -> 16#02A7A; entity("gtdot") -> 16#022D7; entity("gtlPar") -> 16#02995; entity("gtquest") -> 16#02A7C; entity("gtrapprox") -> 16#02A86; entity("gtrarr") -> 16#02978; entity("gtrdot") -> 16#022D7; entity("gtreqless") -> 16#022DB; entity("gtreqqless") -> 16#02A8C; entity("gtrless") -> 16#02277; entity("gtrsim") -> 16#02273; entity("gvertneqq") -> [16#02269, 16#0FE00]; entity("gvnE") -> [16#02269, 16#0FE00]; entity("hArr") -> 16#021D4; entity("hairsp") -> 16#0200A; entity("half") -> 16#000BD; entity("hamilt") -> 16#0210B; entity("hardcy") -> 16#0044A; entity("harr") -> 16#02194; entity("harrcir") -> 16#02948; entity("harrw") -> 16#021AD; entity("hbar") -> 16#0210F; entity("hcirc") -> 16#00125; entity("hearts") -> 16#02665; entity("heartsuit") -> 16#02665; entity("hellip") -> 16#02026; entity("hercon") -> 16#022B9; entity("hfr") -> 16#1D525; entity("hksearow") -> 16#02925; entity("hkswarow") -> 16#02926; entity("hoarr") -> 16#021FF; entity("homtht") -> 16#0223B; entity("hookleftarrow") -> 16#021A9; entity("hookrightarrow") -> 16#021AA; entity("hopf") -> 16#1D559; entity("horbar") -> 16#02015; entity("hscr") -> 16#1D4BD; entity("hslash") -> 16#0210F; entity("hstrok") -> 16#00127; entity("hybull") -> 16#02043; entity("hyphen") -> 16#02010; entity("iacute") -> 16#000ED; entity("ic") -> 16#02063; entity("icirc") -> 16#000EE; entity("icy") -> 16#00438; entity("iecy") -> 16#00435; entity("iexcl") -> 16#000A1; entity("iff") -> 16#021D4; entity("ifr") -> 16#1D526; entity("igrave") -> 16#000EC; entity("ii") -> 16#02148; entity("iiiint") -> 16#02A0C; entity("iiint") -> 16#0222D; entity("iinfin") -> 16#029DC; entity("iiota") -> 16#02129; entity("ijlig") -> 16#00133; entity("imacr") -> 16#0012B; entity("image") -> 16#02111; entity("imagline") -> 16#02110; entity("imagpart") -> 16#02111; entity("imath") -> 16#00131; entity("imof") -> 16#022B7; entity("imped") -> 16#001B5; entity("in") -> 16#02208; entity("incare") -> 16#02105; entity("infin") -> 16#0221E; entity("infintie") -> 16#029DD; entity("inodot") -> 16#00131; entity("int") -> 16#0222B; entity("intcal") -> 16#022BA; entity("integers") -> 16#02124; entity("intercal") -> 16#022BA; entity("intlarhk") -> 16#02A17; entity("intprod") -> 16#02A3C; entity("iocy") -> 16#00451; entity("iogon") -> 16#0012F; entity("iopf") -> 16#1D55A; entity("iota") -> 16#003B9; entity("iprod") -> 16#02A3C; entity("iquest") -> 16#000BF; entity("iscr") -> 16#1D4BE; entity("isin") -> 16#02208; entity("isinE") -> 16#022F9; entity("isindot") -> 16#022F5; entity("isins") -> 16#022F4; entity("isinsv") -> 16#022F3; entity("isinv") -> 16#02208; entity("it") -> 16#02062; entity("itilde") -> 16#00129; entity("iukcy") -> 16#00456; entity("iuml") -> 16#000EF; entity("jcirc") -> 16#00135; entity("jcy") -> 16#00439; entity("jfr") -> 16#1D527; entity("jmath") -> 16#00237; entity("jopf") -> 16#1D55B; entity("jscr") -> 16#1D4BF; entity("jsercy") -> 16#00458; entity("jukcy") -> 16#00454; entity("kappa") -> 16#003BA; entity("kappav") -> 16#003F0; entity("kcedil") -> 16#00137; entity("kcy") -> 16#0043A; entity("kfr") -> 16#1D528; entity("kgreen") -> 16#00138; entity("khcy") -> 16#00445; entity("kjcy") -> 16#0045C; entity("kopf") -> 16#1D55C; entity("kscr") -> 16#1D4C0; entity("lAarr") -> 16#021DA; entity("lArr") -> 16#021D0; entity("lAtail") -> 16#0291B; entity("lBarr") -> 16#0290E; entity("lE") -> 16#02266; entity("lEg") -> 16#02A8B; entity("lHar") -> 16#02962; entity("lacute") -> 16#0013A; entity("laemptyv") -> 16#029B4; entity("lagran") -> 16#02112; entity("lambda") -> 16#003BB; entity("lang") -> 16#027E8; entity("langd") -> 16#02991; entity("langle") -> 16#027E8; entity("lap") -> 16#02A85; entity("laquo") -> 16#000AB; entity("larr") -> 16#02190; entity("larrb") -> 16#021E4; entity("larrbfs") -> 16#0291F; entity("larrfs") -> 16#0291D; entity("larrhk") -> 16#021A9; entity("larrlp") -> 16#021AB; entity("larrpl") -> 16#02939; entity("larrsim") -> 16#02973; entity("larrtl") -> 16#021A2; entity("lat") -> 16#02AAB; entity("latail") -> 16#02919; entity("late") -> 16#02AAD; entity("lates") -> [16#02AAD, 16#0FE00]; entity("lbarr") -> 16#0290C; entity("lbbrk") -> 16#02772; entity("lbrace") -> 16#0007B; entity("lbrack") -> 16#0005B; entity("lbrke") -> 16#0298B; entity("lbrksld") -> 16#0298F; entity("lbrkslu") -> 16#0298D; entity("lcaron") -> 16#0013E; entity("lcedil") -> 16#0013C; entity("lceil") -> 16#02308; entity("lcub") -> 16#0007B; entity("lcy") -> 16#0043B; entity("ldca") -> 16#02936; entity("ldquo") -> 16#0201C; entity("ldquor") -> 16#0201E; entity("ldrdhar") -> 16#02967; entity("ldrushar") -> 16#0294B; entity("ldsh") -> 16#021B2; entity("le") -> 16#02264; entity("leftarrow") -> 16#02190; entity("leftarrowtail") -> 16#021A2; entity("leftharpoondown") -> 16#021BD; entity("leftharpoonup") -> 16#021BC; entity("leftleftarrows") -> 16#021C7; entity("leftrightarrow") -> 16#02194; entity("leftrightarrows") -> 16#021C6; entity("leftrightharpoons") -> 16#021CB; entity("leftrightsquigarrow") -> 16#021AD; entity("leftthreetimes") -> 16#022CB; entity("leg") -> 16#022DA; entity("leq") -> 16#02264; entity("leqq") -> 16#02266; entity("leqslant") -> 16#02A7D; entity("les") -> 16#02A7D; entity("lescc") -> 16#02AA8; entity("lesdot") -> 16#02A7F; entity("lesdoto") -> 16#02A81; entity("lesdotor") -> 16#02A83; entity("lesg") -> [16#022DA, 16#0FE00]; entity("lesges") -> 16#02A93; entity("lessapprox") -> 16#02A85; entity("lessdot") -> 16#022D6; entity("lesseqgtr") -> 16#022DA; entity("lesseqqgtr") -> 16#02A8B; entity("lessgtr") -> 16#02276; entity("lesssim") -> 16#02272; entity("lfisht") -> 16#0297C; entity("lfloor") -> 16#0230A; entity("lfr") -> 16#1D529; entity("lg") -> 16#02276; entity("lgE") -> 16#02A91; entity("lhard") -> 16#021BD; entity("lharu") -> 16#021BC; entity("lharul") -> 16#0296A; entity("lhblk") -> 16#02584; entity("ljcy") -> 16#00459; entity("ll") -> 16#0226A; entity("llarr") -> 16#021C7; entity("llcorner") -> 16#0231E; entity("llhard") -> 16#0296B; entity("lltri") -> 16#025FA; entity("lmidot") -> 16#00140; entity("lmoust") -> 16#023B0; entity("lmoustache") -> 16#023B0; entity("lnE") -> 16#02268; entity("lnap") -> 16#02A89; entity("lnapprox") -> 16#02A89; entity("lne") -> 16#02A87; entity("lneq") -> 16#02A87; entity("lneqq") -> 16#02268; entity("lnsim") -> 16#022E6; entity("loang") -> 16#027EC; entity("loarr") -> 16#021FD; entity("lobrk") -> 16#027E6; entity("longleftarrow") -> 16#027F5; entity("longleftrightarrow") -> 16#027F7; entity("longmapsto") -> 16#027FC; entity("longrightarrow") -> 16#027F6; entity("looparrowleft") -> 16#021AB; entity("looparrowright") -> 16#021AC; entity("lopar") -> 16#02985; entity("lopf") -> 16#1D55D; entity("loplus") -> 16#02A2D; entity("lotimes") -> 16#02A34; entity("lowast") -> 16#02217; entity("lowbar") -> 16#0005F; entity("loz") -> 16#025CA; entity("lozenge") -> 16#025CA; entity("lozf") -> 16#029EB; entity("lpar") -> 16#00028; entity("lparlt") -> 16#02993; entity("lrarr") -> 16#021C6; entity("lrcorner") -> 16#0231F; entity("lrhar") -> 16#021CB; entity("lrhard") -> 16#0296D; entity("lrm") -> 16#0200E; entity("lrtri") -> 16#022BF; entity("lsaquo") -> 16#02039; entity("lscr") -> 16#1D4C1; entity("lsh") -> 16#021B0; entity("lsim") -> 16#02272; entity("lsime") -> 16#02A8D; entity("lsimg") -> 16#02A8F; entity("lsqb") -> 16#0005B; entity("lsquo") -> 16#02018; entity("lsquor") -> 16#0201A; entity("lstrok") -> 16#00142; entity("lt") -> 16#0003C; entity("ltcc") -> 16#02AA6; entity("ltcir") -> 16#02A79; entity("ltdot") -> 16#022D6; entity("lthree") -> 16#022CB; entity("ltimes") -> 16#022C9; entity("ltlarr") -> 16#02976; entity("ltquest") -> 16#02A7B; entity("ltrPar") -> 16#02996; entity("ltri") -> 16#025C3; entity("ltrie") -> 16#022B4; entity("ltrif") -> 16#025C2; entity("lurdshar") -> 16#0294A; entity("luruhar") -> 16#02966; entity("lvertneqq") -> [16#02268, 16#0FE00]; entity("lvnE") -> [16#02268, 16#0FE00]; entity("mDDot") -> 16#0223A; entity("macr") -> 16#000AF; entity("male") -> 16#02642; entity("malt") -> 16#02720; entity("maltese") -> 16#02720; entity("map") -> 16#021A6; entity("mapsto") -> 16#021A6; entity("mapstodown") -> 16#021A7; entity("mapstoleft") -> 16#021A4; entity("mapstoup") -> 16#021A5; entity("marker") -> 16#025AE; entity("mcomma") -> 16#02A29; entity("mcy") -> 16#0043C; entity("mdash") -> 16#02014; entity("measuredangle") -> 16#02221; entity("mfr") -> 16#1D52A; entity("mho") -> 16#02127; entity("micro") -> 16#000B5; entity("mid") -> 16#02223; entity("midast") -> 16#0002A; entity("midcir") -> 16#02AF0; entity("middot") -> 16#000B7; entity("minus") -> 16#02212; entity("minusb") -> 16#0229F; entity("minusd") -> 16#02238; entity("minusdu") -> 16#02A2A; entity("mlcp") -> 16#02ADB; entity("mldr") -> 16#02026; entity("mnplus") -> 16#02213; entity("models") -> 16#022A7; entity("mopf") -> 16#1D55E; entity("mp") -> 16#02213; entity("mscr") -> 16#1D4C2; entity("mstpos") -> 16#0223E; entity("mu") -> 16#003BC; entity("multimap") -> 16#022B8; entity("mumap") -> 16#022B8; entity("nGg") -> [16#022D9, 16#00338]; entity("nGt") -> [16#0226B, 16#020D2]; entity("nGtv") -> [16#0226B, 16#00338]; entity("nLeftarrow") -> 16#021CD; entity("nLeftrightarrow") -> 16#021CE; entity("nLl") -> [16#022D8, 16#00338]; entity("nLt") -> [16#0226A, 16#020D2]; entity("nLtv") -> [16#0226A, 16#00338]; entity("nRightarrow") -> 16#021CF; entity("nVDash") -> 16#022AF; entity("nVdash") -> 16#022AE; entity("nabla") -> 16#02207; entity("nacute") -> 16#00144; entity("nang") -> [16#02220, 16#020D2]; entity("nap") -> 16#02249; entity("napE") -> [16#02A70, 16#00338]; entity("napid") -> [16#0224B, 16#00338]; entity("napos") -> 16#00149; entity("napprox") -> 16#02249; entity("natur") -> 16#0266E; entity("natural") -> 16#0266E; entity("naturals") -> 16#02115; entity("nbsp") -> 16#000A0; entity("nbump") -> [16#0224E, 16#00338]; entity("nbumpe") -> [16#0224F, 16#00338]; entity("ncap") -> 16#02A43; entity("ncaron") -> 16#00148; entity("ncedil") -> 16#00146; entity("ncong") -> 16#02247; entity("ncongdot") -> [16#02A6D, 16#00338]; entity("ncup") -> 16#02A42; entity("ncy") -> 16#0043D; entity("ndash") -> 16#02013; entity("ne") -> 16#02260; entity("neArr") -> 16#021D7; entity("nearhk") -> 16#02924; entity("nearr") -> 16#02197; entity("nearrow") -> 16#02197; entity("nedot") -> [16#02250, 16#00338]; entity("nequiv") -> 16#02262; entity("nesear") -> 16#02928; entity("nesim") -> [16#02242, 16#00338]; entity("nexist") -> 16#02204; entity("nexists") -> 16#02204; entity("nfr") -> 16#1D52B; entity("ngE") -> [16#02267, 16#00338]; entity("nge") -> 16#02271; entity("ngeq") -> 16#02271; entity("ngeqq") -> [16#02267, 16#00338]; entity("ngeqslant") -> [16#02A7E, 16#00338]; entity("nges") -> [16#02A7E, 16#00338]; entity("ngsim") -> 16#02275; entity("ngt") -> 16#0226F; entity("ngtr") -> 16#0226F; entity("nhArr") -> 16#021CE; entity("nharr") -> 16#021AE; entity("nhpar") -> 16#02AF2; entity("ni") -> 16#0220B; entity("nis") -> 16#022FC; entity("nisd") -> 16#022FA; entity("niv") -> 16#0220B; entity("njcy") -> 16#0045A; entity("nlArr") -> 16#021CD; entity("nlE") -> [16#02266, 16#00338]; entity("nlarr") -> 16#0219A; entity("nldr") -> 16#02025; entity("nle") -> 16#02270; entity("nleftarrow") -> 16#0219A; entity("nleftrightarrow") -> 16#021AE; entity("nleq") -> 16#02270; entity("nleqq") -> [16#02266, 16#00338]; entity("nleqslant") -> [16#02A7D, 16#00338]; entity("nles") -> [16#02A7D, 16#00338]; entity("nless") -> 16#0226E; entity("nlsim") -> 16#02274; entity("nlt") -> 16#0226E; entity("nltri") -> 16#022EA; entity("nltrie") -> 16#022EC; entity("nmid") -> 16#02224; entity("nopf") -> 16#1D55F; entity("not") -> 16#000AC; entity("notin") -> 16#02209; entity("notinE") -> [16#022F9, 16#00338]; entity("notindot") -> [16#022F5, 16#00338]; entity("notinva") -> 16#02209; entity("notinvb") -> 16#022F7; entity("notinvc") -> 16#022F6; entity("notni") -> 16#0220C; entity("notniva") -> 16#0220C; entity("notnivb") -> 16#022FE; entity("notnivc") -> 16#022FD; entity("npar") -> 16#02226; entity("nparallel") -> 16#02226; entity("nparsl") -> [16#02AFD, 16#020E5]; entity("npart") -> [16#02202, 16#00338]; entity("npolint") -> 16#02A14; entity("npr") -> 16#02280; entity("nprcue") -> 16#022E0; entity("npre") -> [16#02AAF, 16#00338]; entity("nprec") -> 16#02280; entity("npreceq") -> [16#02AAF, 16#00338]; entity("nrArr") -> 16#021CF; entity("nrarr") -> 16#0219B; entity("nrarrc") -> [16#02933, 16#00338]; entity("nrarrw") -> [16#0219D, 16#00338]; entity("nrightarrow") -> 16#0219B; entity("nrtri") -> 16#022EB; entity("nrtrie") -> 16#022ED; entity("nsc") -> 16#02281; entity("nsccue") -> 16#022E1; entity("nsce") -> [16#02AB0, 16#00338]; entity("nscr") -> 16#1D4C3; entity("nshortmid") -> 16#02224; entity("nshortparallel") -> 16#02226; entity("nsim") -> 16#02241; entity("nsime") -> 16#02244; entity("nsimeq") -> 16#02244; entity("nsmid") -> 16#02224; entity("nspar") -> 16#02226; entity("nsqsube") -> 16#022E2; entity("nsqsupe") -> 16#022E3; entity("nsub") -> 16#02284; entity("nsubE") -> [16#02AC5, 16#00338]; entity("nsube") -> 16#02288; entity("nsubset") -> [16#02282, 16#020D2]; entity("nsubseteq") -> 16#02288; entity("nsubseteqq") -> [16#02AC5, 16#00338]; entity("nsucc") -> 16#02281; entity("nsucceq") -> [16#02AB0, 16#00338]; entity("nsup") -> 16#02285; entity("nsupE") -> [16#02AC6, 16#00338]; entity("nsupe") -> 16#02289; entity("nsupset") -> [16#02283, 16#020D2]; entity("nsupseteq") -> 16#02289; entity("nsupseteqq") -> [16#02AC6, 16#00338]; entity("ntgl") -> 16#02279; entity("ntilde") -> 16#000F1; entity("ntlg") -> 16#02278; entity("ntriangleleft") -> 16#022EA; entity("ntrianglelefteq") -> 16#022EC; entity("ntriangleright") -> 16#022EB; entity("ntrianglerighteq") -> 16#022ED; entity("nu") -> 16#003BD; entity("num") -> 16#00023; entity("numero") -> 16#02116; entity("numsp") -> 16#02007; entity("nvDash") -> 16#022AD; entity("nvHarr") -> 16#02904; entity("nvap") -> [16#0224D, 16#020D2]; entity("nvdash") -> 16#022AC; entity("nvge") -> [16#02265, 16#020D2]; entity("nvgt") -> [16#0003E, 16#020D2]; entity("nvinfin") -> 16#029DE; entity("nvlArr") -> 16#02902; entity("nvle") -> [16#02264, 16#020D2]; entity("nvlt") -> [16#0003C, 16#020D2]; entity("nvltrie") -> [16#022B4, 16#020D2]; entity("nvrArr") -> 16#02903; entity("nvrtrie") -> [16#022B5, 16#020D2]; entity("nvsim") -> [16#0223C, 16#020D2]; entity("nwArr") -> 16#021D6; entity("nwarhk") -> 16#02923; entity("nwarr") -> 16#02196; entity("nwarrow") -> 16#02196; entity("nwnear") -> 16#02927; entity("oS") -> 16#024C8; entity("oacute") -> 16#000F3; entity("oast") -> 16#0229B; entity("ocir") -> 16#0229A; entity("ocirc") -> 16#000F4; entity("ocy") -> 16#0043E; entity("odash") -> 16#0229D; entity("odblac") -> 16#00151; entity("odiv") -> 16#02A38; entity("odot") -> 16#02299; entity("odsold") -> 16#029BC; entity("oelig") -> 16#00153; entity("ofcir") -> 16#029BF; entity("ofr") -> 16#1D52C; entity("ogon") -> 16#002DB; entity("ograve") -> 16#000F2; entity("ogt") -> 16#029C1; entity("ohbar") -> 16#029B5; entity("ohm") -> 16#003A9; entity("oint") -> 16#0222E; entity("olarr") -> 16#021BA; entity("olcir") -> 16#029BE; entity("olcross") -> 16#029BB; entity("oline") -> 16#0203E; entity("olt") -> 16#029C0; entity("omacr") -> 16#0014D; entity("omega") -> 16#003C9; entity("omicron") -> 16#003BF; entity("omid") -> 16#029B6; entity("ominus") -> 16#02296; entity("oopf") -> 16#1D560; entity("opar") -> 16#029B7; entity("operp") -> 16#029B9; entity("oplus") -> 16#02295; entity("or") -> 16#02228; entity("orarr") -> 16#021BB; entity("ord") -> 16#02A5D; entity("order") -> 16#02134; entity("orderof") -> 16#02134; entity("ordf") -> 16#000AA; entity("ordm") -> 16#000BA; entity("origof") -> 16#022B6; entity("oror") -> 16#02A56; entity("orslope") -> 16#02A57; entity("orv") -> 16#02A5B; entity("oscr") -> 16#02134; entity("oslash") -> 16#000F8; entity("osol") -> 16#02298; entity("otilde") -> 16#000F5; entity("otimes") -> 16#02297; entity("otimesas") -> 16#02A36; entity("ouml") -> 16#000F6; entity("ovbar") -> 16#0233D; entity("par") -> 16#02225; entity("para") -> 16#000B6; entity("parallel") -> 16#02225; entity("parsim") -> 16#02AF3; entity("parsl") -> 16#02AFD; entity("part") -> 16#02202; entity("pcy") -> 16#0043F; entity("percnt") -> 16#00025; entity("period") -> 16#0002E; entity("permil") -> 16#02030; entity("perp") -> 16#022A5; entity("pertenk") -> 16#02031; entity("pfr") -> 16#1D52D; entity("phi") -> 16#003C6; entity("phiv") -> 16#003D5; entity("phmmat") -> 16#02133; entity("phone") -> 16#0260E; entity("pi") -> 16#003C0; entity("pitchfork") -> 16#022D4; entity("piv") -> 16#003D6; entity("planck") -> 16#0210F; entity("planckh") -> 16#0210E; entity("plankv") -> 16#0210F; entity("plus") -> 16#0002B; entity("plusacir") -> 16#02A23; entity("plusb") -> 16#0229E; entity("pluscir") -> 16#02A22; entity("plusdo") -> 16#02214; entity("plusdu") -> 16#02A25; entity("pluse") -> 16#02A72; entity("plusmn") -> 16#000B1; entity("plussim") -> 16#02A26; entity("plustwo") -> 16#02A27; entity("pm") -> 16#000B1; entity("pointint") -> 16#02A15; entity("popf") -> 16#1D561; entity("pound") -> 16#000A3; entity("pr") -> 16#0227A; entity("prE") -> 16#02AB3; entity("prap") -> 16#02AB7; entity("prcue") -> 16#0227C; entity("pre") -> 16#02AAF; entity("prec") -> 16#0227A; entity("precapprox") -> 16#02AB7; entity("preccurlyeq") -> 16#0227C; entity("preceq") -> 16#02AAF; entity("precnapprox") -> 16#02AB9; entity("precneqq") -> 16#02AB5; entity("precnsim") -> 16#022E8; entity("precsim") -> 16#0227E; entity("prime") -> 16#02032; entity("primes") -> 16#02119; entity("prnE") -> 16#02AB5; entity("prnap") -> 16#02AB9; entity("prnsim") -> 16#022E8; entity("prod") -> 16#0220F; entity("profalar") -> 16#0232E; entity("profline") -> 16#02312; entity("profsurf") -> 16#02313; entity("prop") -> 16#0221D; entity("propto") -> 16#0221D; entity("prsim") -> 16#0227E; entity("prurel") -> 16#022B0; entity("pscr") -> 16#1D4C5; entity("psi") -> 16#003C8; entity("puncsp") -> 16#02008; entity("qfr") -> 16#1D52E; entity("qint") -> 16#02A0C; entity("qopf") -> 16#1D562; entity("qprime") -> 16#02057; entity("qscr") -> 16#1D4C6; entity("quaternions") -> 16#0210D; entity("quatint") -> 16#02A16; entity("quest") -> 16#0003F; entity("questeq") -> 16#0225F; entity("quot") -> 16#00022; entity("rAarr") -> 16#021DB; entity("rArr") -> 16#021D2; entity("rAtail") -> 16#0291C; entity("rBarr") -> 16#0290F; entity("rHar") -> 16#02964; entity("race") -> [16#0223D, 16#00331]; entity("racute") -> 16#00155; entity("radic") -> 16#0221A; entity("raemptyv") -> 16#029B3; entity("rang") -> 16#027E9; entity("rangd") -> 16#02992; entity("range") -> 16#029A5; entity("rangle") -> 16#027E9; entity("raquo") -> 16#000BB; entity("rarr") -> 16#02192; entity("rarrap") -> 16#02975; entity("rarrb") -> 16#021E5; entity("rarrbfs") -> 16#02920; entity("rarrc") -> 16#02933; entity("rarrfs") -> 16#0291E; entity("rarrhk") -> 16#021AA; entity("rarrlp") -> 16#021AC; entity("rarrpl") -> 16#02945; entity("rarrsim") -> 16#02974; entity("rarrtl") -> 16#021A3; entity("rarrw") -> 16#0219D; entity("ratail") -> 16#0291A; entity("ratio") -> 16#02236; entity("rationals") -> 16#0211A; entity("rbarr") -> 16#0290D; entity("rbbrk") -> 16#02773; entity("rbrace") -> 16#0007D; entity("rbrack") -> 16#0005D; entity("rbrke") -> 16#0298C; entity("rbrksld") -> 16#0298E; entity("rbrkslu") -> 16#02990; entity("rcaron") -> 16#00159; entity("rcedil") -> 16#00157; entity("rceil") -> 16#02309; entity("rcub") -> 16#0007D; entity("rcy") -> 16#00440; entity("rdca") -> 16#02937; entity("rdldhar") -> 16#02969; entity("rdquo") -> 16#0201D; entity("rdquor") -> 16#0201D; entity("rdsh") -> 16#021B3; entity("real") -> 16#0211C; entity("realine") -> 16#0211B; entity("realpart") -> 16#0211C; entity("reals") -> 16#0211D; entity("rect") -> 16#025AD; entity("reg") -> 16#000AE; entity("rfisht") -> 16#0297D; entity("rfloor") -> 16#0230B; entity("rfr") -> 16#1D52F; entity("rhard") -> 16#021C1; entity("rharu") -> 16#021C0; entity("rharul") -> 16#0296C; entity("rho") -> 16#003C1; entity("rhov") -> 16#003F1; entity("rightarrow") -> 16#02192; entity("rightarrowtail") -> 16#021A3; entity("rightharpoondown") -> 16#021C1; entity("rightharpoonup") -> 16#021C0; entity("rightleftarrows") -> 16#021C4; entity("rightleftharpoons") -> 16#021CC; entity("rightrightarrows") -> 16#021C9; entity("rightsquigarrow") -> 16#0219D; entity("rightthreetimes") -> 16#022CC; entity("ring") -> 16#002DA; entity("risingdotseq") -> 16#02253; entity("rlarr") -> 16#021C4; entity("rlhar") -> 16#021CC; entity("rlm") -> 16#0200F; entity("rmoust") -> 16#023B1; entity("rmoustache") -> 16#023B1; entity("rnmid") -> 16#02AEE; entity("roang") -> 16#027ED; entity("roarr") -> 16#021FE; entity("robrk") -> 16#027E7; entity("ropar") -> 16#02986; entity("ropf") -> 16#1D563; entity("roplus") -> 16#02A2E; entity("rotimes") -> 16#02A35; entity("rpar") -> 16#00029; entity("rpargt") -> 16#02994; entity("rppolint") -> 16#02A12; entity("rrarr") -> 16#021C9; entity("rsaquo") -> 16#0203A; entity("rscr") -> 16#1D4C7; entity("rsh") -> 16#021B1; entity("rsqb") -> 16#0005D; entity("rsquo") -> 16#02019; entity("rsquor") -> 16#02019; entity("rthree") -> 16#022CC; entity("rtimes") -> 16#022CA; entity("rtri") -> 16#025B9; entity("rtrie") -> 16#022B5; entity("rtrif") -> 16#025B8; entity("rtriltri") -> 16#029CE; entity("ruluhar") -> 16#02968; entity("rx") -> 16#0211E; entity("sacute") -> 16#0015B; entity("sbquo") -> 16#0201A; entity("sc") -> 16#0227B; entity("scE") -> 16#02AB4; entity("scap") -> 16#02AB8; entity("scaron") -> 16#00161; entity("sccue") -> 16#0227D; entity("sce") -> 16#02AB0; entity("scedil") -> 16#0015F; entity("scirc") -> 16#0015D; entity("scnE") -> 16#02AB6; entity("scnap") -> 16#02ABA; entity("scnsim") -> 16#022E9; entity("scpolint") -> 16#02A13; entity("scsim") -> 16#0227F; entity("scy") -> 16#00441; entity("sdot") -> 16#022C5; entity("sdotb") -> 16#022A1; entity("sdote") -> 16#02A66; entity("seArr") -> 16#021D8; entity("searhk") -> 16#02925; entity("searr") -> 16#02198; entity("searrow") -> 16#02198; entity("sect") -> 16#000A7; entity("semi") -> 16#0003B; entity("seswar") -> 16#02929; entity("setminus") -> 16#02216; entity("setmn") -> 16#02216; entity("sext") -> 16#02736; entity("sfr") -> 16#1D530; entity("sfrown") -> 16#02322; entity("sharp") -> 16#0266F; entity("shchcy") -> 16#00449; entity("shcy") -> 16#00448; entity("shortmid") -> 16#02223; entity("shortparallel") -> 16#02225; entity("shy") -> 16#000AD; entity("sigma") -> 16#003C3; entity("sigmaf") -> 16#003C2; entity("sigmav") -> 16#003C2; entity("sim") -> 16#0223C; entity("simdot") -> 16#02A6A; entity("sime") -> 16#02243; entity("simeq") -> 16#02243; entity("simg") -> 16#02A9E; entity("simgE") -> 16#02AA0; entity("siml") -> 16#02A9D; entity("simlE") -> 16#02A9F; entity("simne") -> 16#02246; entity("simplus") -> 16#02A24; entity("simrarr") -> 16#02972; entity("slarr") -> 16#02190; entity("smallsetminus") -> 16#02216; entity("smashp") -> 16#02A33; entity("smeparsl") -> 16#029E4; entity("smid") -> 16#02223; entity("smile") -> 16#02323; entity("smt") -> 16#02AAA; entity("smte") -> 16#02AAC; entity("smtes") -> [16#02AAC, 16#0FE00]; entity("softcy") -> 16#0044C; entity("sol") -> 16#0002F; entity("solb") -> 16#029C4; entity("solbar") -> 16#0233F; entity("sopf") -> 16#1D564; entity("spades") -> 16#02660; entity("spadesuit") -> 16#02660; entity("spar") -> 16#02225; entity("sqcap") -> 16#02293; entity("sqcaps") -> [16#02293, 16#0FE00]; entity("sqcup") -> 16#02294; entity("sqcups") -> [16#02294, 16#0FE00]; entity("sqsub") -> 16#0228F; entity("sqsube") -> 16#02291; entity("sqsubset") -> 16#0228F; entity("sqsubseteq") -> 16#02291; entity("sqsup") -> 16#02290; entity("sqsupe") -> 16#02292; entity("sqsupset") -> 16#02290; entity("sqsupseteq") -> 16#02292; entity("squ") -> 16#025A1; entity("square") -> 16#025A1; entity("squarf") -> 16#025AA; entity("squf") -> 16#025AA; entity("srarr") -> 16#02192; entity("sscr") -> 16#1D4C8; entity("ssetmn") -> 16#02216; entity("ssmile") -> 16#02323; entity("sstarf") -> 16#022C6; entity("star") -> 16#02606; entity("starf") -> 16#02605; entity("straightepsilon") -> 16#003F5; entity("straightphi") -> 16#003D5; entity("strns") -> 16#000AF; entity("sub") -> 16#02282; entity("subE") -> 16#02AC5; entity("subdot") -> 16#02ABD; entity("sube") -> 16#02286; entity("subedot") -> 16#02AC3; entity("submult") -> 16#02AC1; entity("subnE") -> 16#02ACB; entity("subne") -> 16#0228A; entity("subplus") -> 16#02ABF; entity("subrarr") -> 16#02979; entity("subset") -> 16#02282; entity("subseteq") -> 16#02286; entity("subseteqq") -> 16#02AC5; entity("subsetneq") -> 16#0228A; entity("subsetneqq") -> 16#02ACB; entity("subsim") -> 16#02AC7; entity("subsub") -> 16#02AD5; entity("subsup") -> 16#02AD3; entity("succ") -> 16#0227B; entity("succapprox") -> 16#02AB8; entity("succcurlyeq") -> 16#0227D; entity("succeq") -> 16#02AB0; entity("succnapprox") -> 16#02ABA; entity("succneqq") -> 16#02AB6; entity("succnsim") -> 16#022E9; entity("succsim") -> 16#0227F; entity("sum") -> 16#02211; entity("sung") -> 16#0266A; entity("sup") -> 16#02283; entity("sup1") -> 16#000B9; entity("sup2") -> 16#000B2; entity("sup3") -> 16#000B3; entity("supE") -> 16#02AC6; entity("supdot") -> 16#02ABE; entity("supdsub") -> 16#02AD8; entity("supe") -> 16#02287; entity("supedot") -> 16#02AC4; entity("suphsol") -> 16#027C9; entity("suphsub") -> 16#02AD7; entity("suplarr") -> 16#0297B; entity("supmult") -> 16#02AC2; entity("supnE") -> 16#02ACC; entity("supne") -> 16#0228B; entity("supplus") -> 16#02AC0; entity("supset") -> 16#02283; entity("supseteq") -> 16#02287; entity("supseteqq") -> 16#02AC6; entity("supsetneq") -> 16#0228B; entity("supsetneqq") -> 16#02ACC; entity("supsim") -> 16#02AC8; entity("supsub") -> 16#02AD4; entity("supsup") -> 16#02AD6; entity("swArr") -> 16#021D9; entity("swarhk") -> 16#02926; entity("swarr") -> 16#02199; entity("swarrow") -> 16#02199; entity("swnwar") -> 16#0292A; entity("szlig") -> 16#000DF; entity("target") -> 16#02316; entity("tau") -> 16#003C4; entity("tbrk") -> 16#023B4; entity("tcaron") -> 16#00165; entity("tcedil") -> 16#00163; entity("tcy") -> 16#00442; entity("tdot") -> 16#020DB; entity("telrec") -> 16#02315; entity("tfr") -> 16#1D531; entity("there4") -> 16#02234; entity("therefore") -> 16#02234; entity("theta") -> 16#003B8; entity("thetasym") -> 16#003D1; entity("thetav") -> 16#003D1; entity("thickapprox") -> 16#02248; entity("thicksim") -> 16#0223C; entity("thinsp") -> 16#02009; entity("thkap") -> 16#02248; entity("thksim") -> 16#0223C; entity("thorn") -> 16#000FE; entity("tilde") -> 16#002DC; entity("times") -> 16#000D7; entity("timesb") -> 16#022A0; entity("timesbar") -> 16#02A31; entity("timesd") -> 16#02A30; entity("tint") -> 16#0222D; entity("toea") -> 16#02928; entity("top") -> 16#022A4; entity("topbot") -> 16#02336; entity("topcir") -> 16#02AF1; entity("topf") -> 16#1D565; entity("topfork") -> 16#02ADA; entity("tosa") -> 16#02929; entity("tprime") -> 16#02034; entity("trade") -> 16#02122; entity("triangle") -> 16#025B5; entity("triangledown") -> 16#025BF; entity("triangleleft") -> 16#025C3; entity("trianglelefteq") -> 16#022B4; entity("triangleq") -> 16#0225C; entity("triangleright") -> 16#025B9; entity("trianglerighteq") -> 16#022B5; entity("tridot") -> 16#025EC; entity("trie") -> 16#0225C; entity("triminus") -> 16#02A3A; entity("triplus") -> 16#02A39; entity("trisb") -> 16#029CD; entity("tritime") -> 16#02A3B; entity("trpezium") -> 16#023E2; entity("tscr") -> 16#1D4C9; entity("tscy") -> 16#00446; entity("tshcy") -> 16#0045B; entity("tstrok") -> 16#00167; entity("twixt") -> 16#0226C; entity("twoheadleftarrow") -> 16#0219E; entity("twoheadrightarrow") -> 16#021A0; entity("uArr") -> 16#021D1; entity("uHar") -> 16#02963; entity("uacute") -> 16#000FA; entity("uarr") -> 16#02191; entity("ubrcy") -> 16#0045E; entity("ubreve") -> 16#0016D; entity("ucirc") -> 16#000FB; entity("ucy") -> 16#00443; entity("udarr") -> 16#021C5; entity("udblac") -> 16#00171; entity("udhar") -> 16#0296E; entity("ufisht") -> 16#0297E; entity("ufr") -> 16#1D532; entity("ugrave") -> 16#000F9; entity("uharl") -> 16#021BF; entity("uharr") -> 16#021BE; entity("uhblk") -> 16#02580; entity("ulcorn") -> 16#0231C; entity("ulcorner") -> 16#0231C; entity("ulcrop") -> 16#0230F; entity("ultri") -> 16#025F8; entity("umacr") -> 16#0016B; entity("uml") -> 16#000A8; entity("uogon") -> 16#00173; entity("uopf") -> 16#1D566; entity("uparrow") -> 16#02191; entity("updownarrow") -> 16#02195; entity("upharpoonleft") -> 16#021BF; entity("upharpoonright") -> 16#021BE; entity("uplus") -> 16#0228E; entity("upsi") -> 16#003C5; entity("upsih") -> 16#003D2; entity("upsilon") -> 16#003C5; entity("upuparrows") -> 16#021C8; entity("urcorn") -> 16#0231D; entity("urcorner") -> 16#0231D; entity("urcrop") -> 16#0230E; entity("uring") -> 16#0016F; entity("urtri") -> 16#025F9; entity("uscr") -> 16#1D4CA; entity("utdot") -> 16#022F0; entity("utilde") -> 16#00169; entity("utri") -> 16#025B5; entity("utrif") -> 16#025B4; entity("uuarr") -> 16#021C8; entity("uuml") -> 16#000FC; entity("uwangle") -> 16#029A7; entity("vArr") -> 16#021D5; entity("vBar") -> 16#02AE8; entity("vBarv") -> 16#02AE9; entity("vDash") -> 16#022A8; entity("vangrt") -> 16#0299C; entity("varepsilon") -> 16#003F5; entity("varkappa") -> 16#003F0; entity("varnothing") -> 16#02205; entity("varphi") -> 16#003D5; entity("varpi") -> 16#003D6; entity("varpropto") -> 16#0221D; entity("varr") -> 16#02195; entity("varrho") -> 16#003F1; entity("varsigma") -> 16#003C2; entity("varsubsetneq") -> [16#0228A, 16#0FE00]; entity("varsubsetneqq") -> [16#02ACB, 16#0FE00]; entity("varsupsetneq") -> [16#0228B, 16#0FE00]; entity("varsupsetneqq") -> [16#02ACC, 16#0FE00]; entity("vartheta") -> 16#003D1; entity("vartriangleleft") -> 16#022B2; entity("vartriangleright") -> 16#022B3; entity("vcy") -> 16#00432; entity("vdash") -> 16#022A2; entity("vee") -> 16#02228; entity("veebar") -> 16#022BB; entity("veeeq") -> 16#0225A; entity("vellip") -> 16#022EE; entity("verbar") -> 16#0007C; entity("vert") -> 16#0007C; entity("vfr") -> 16#1D533; entity("vltri") -> 16#022B2; entity("vnsub") -> [16#02282, 16#020D2]; entity("vnsup") -> [16#02283, 16#020D2]; entity("vopf") -> 16#1D567; entity("vprop") -> 16#0221D; entity("vrtri") -> 16#022B3; entity("vscr") -> 16#1D4CB; entity("vsubnE") -> [16#02ACB, 16#0FE00]; entity("vsubne") -> [16#0228A, 16#0FE00]; entity("vsupnE") -> [16#02ACC, 16#0FE00]; entity("vsupne") -> [16#0228B, 16#0FE00]; entity("vzigzag") -> 16#0299A; entity("wcirc") -> 16#00175; entity("wedbar") -> 16#02A5F; entity("wedge") -> 16#02227; entity("wedgeq") -> 16#02259; entity("weierp") -> 16#02118; entity("wfr") -> 16#1D534; entity("wopf") -> 16#1D568; entity("wp") -> 16#02118; entity("wr") -> 16#02240; entity("wreath") -> 16#02240; entity("wscr") -> 16#1D4CC; entity("xcap") -> 16#022C2; entity("xcirc") -> 16#025EF; entity("xcup") -> 16#022C3; entity("xdtri") -> 16#025BD; entity("xfr") -> 16#1D535; entity("xhArr") -> 16#027FA; entity("xharr") -> 16#027F7; entity("xi") -> 16#003BE; entity("xlArr") -> 16#027F8; entity("xlarr") -> 16#027F5; entity("xmap") -> 16#027FC; entity("xnis") -> 16#022FB; entity("xodot") -> 16#02A00; entity("xopf") -> 16#1D569; entity("xoplus") -> 16#02A01; entity("xotime") -> 16#02A02; entity("xrArr") -> 16#027F9; entity("xrarr") -> 16#027F6; entity("xscr") -> 16#1D4CD; entity("xsqcup") -> 16#02A06; entity("xuplus") -> 16#02A04; entity("xutri") -> 16#025B3; entity("xvee") -> 16#022C1; entity("xwedge") -> 16#022C0; entity("yacute") -> 16#000FD; entity("yacy") -> 16#0044F; entity("ycirc") -> 16#00177; entity("ycy") -> 16#0044B; entity("yen") -> 16#000A5; entity("yfr") -> 16#1D536; entity("yicy") -> 16#00457; entity("yopf") -> 16#1D56A; entity("yscr") -> 16#1D4CE; entity("yucy") -> 16#0044E; entity("yuml") -> 16#000FF; entity("zacute") -> 16#0017A; entity("zcaron") -> 16#0017E; entity("zcy") -> 16#00437; entity("zdot") -> 16#0017C; entity("zeetrf") -> 16#02128; entity("zeta") -> 16#003B6; entity("zfr") -> 16#1D537; entity("zhcy") -> 16#00436; entity("zigrarr") -> 16#021DD; entity("zopf") -> 16#1D56B; entity("zscr") -> 16#1D4CF; entity("zwj") -> 16#0200D; entity("zwnj") -> 16#0200C; entity(_) -> undefined. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). exhaustive_entity_test() -> T = mochiweb_cover:clause_lookup_table(?MODULE, entity), [?assertEqual(V, entity(K)) || {K, V} <- T]. charref_test() -> 1234 = charref("#1234"), 255 = charref("#xfF"), 255 = charref(<<"#XFf">>), 38 = charref("amp"), 38 = charref(<<"amp">>), undefined = charref("not_an_entity"), undefined = charref("#not_an_entity"), undefined = charref("#xnot_an_entity"), ok. -endif. tsung-1.8.0/src/lib/mochiutf8.erl0000644000201100017670000003051114377756736016345 0ustar nniclausdream%% @copyright 2010 Mochi Media, Inc. %% @author Bob Ippolito %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Algorithm to convert any binary to a valid UTF-8 sequence by ignoring %% invalid bytes. -module(mochiutf8). -export([valid_utf8_bytes/1, codepoint_to_bytes/1, codepoints_to_bytes/1]). -export([bytes_to_codepoints/1, bytes_foldl/3, codepoint_foldl/3]). -export([read_codepoint/1, len/1]). %% External API -type unichar_low() :: 0..16#d7ff. -type unichar_high() :: 16#e000..16#10ffff. -type unichar() :: unichar_low() | unichar_high(). -spec codepoint_to_bytes(unichar()) -> binary(). %% @doc Convert a unicode codepoint to UTF-8 bytes. codepoint_to_bytes(C) when (C >= 16#00 andalso C =< 16#7f) -> %% U+0000 - U+007F - 7 bits <>; codepoint_to_bytes(C) when (C >= 16#080 andalso C =< 16#07FF) -> %% U+0080 - U+07FF - 11 bits <<0:5, B1:5, B0:6>> = <>, <<2#110:3, B1:5, 2#10:2, B0:6>>; codepoint_to_bytes(C) when (C >= 16#0800 andalso C =< 16#FFFF) andalso (C < 16#D800 orelse C > 16#DFFF) -> %% U+0800 - U+FFFF - 16 bits (excluding UTC-16 surrogate code points) <> = <>, <<2#1110:4, B2:4, 2#10:2, B1:6, 2#10:2, B0:6>>; codepoint_to_bytes(C) when (C >= 16#010000 andalso C =< 16#10FFFF) -> %% U+10000 - U+10FFFF - 21 bits <<0:3, B3:3, B2:6, B1:6, B0:6>> = <>, <<2#11110:5, B3:3, 2#10:2, B2:6, 2#10:2, B1:6, 2#10:2, B0:6>>. -spec codepoints_to_bytes([unichar()]) -> binary(). %% @doc Convert a list of codepoints to a UTF-8 binary. codepoints_to_bytes(L) -> <<<<(codepoint_to_bytes(C))/binary>> || C <- L>>. -spec read_codepoint(binary()) -> {unichar(), binary(), binary()}. read_codepoint(Bin = <<2#0:1, C:7, Rest/binary>>) -> %% U+0000 - U+007F - 7 bits <> = Bin, {C, B, Rest}; read_codepoint(Bin = <<2#110:3, B1:5, 2#10:2, B0:6, Rest/binary>>) -> %% U+0080 - U+07FF - 11 bits case <> of <> when C >= 16#80 -> <> = Bin, {C, B, Rest} end; read_codepoint(Bin = <<2#1110:4, B2:4, 2#10:2, B1:6, 2#10:2, B0:6, Rest/binary>>) -> %% U+0800 - U+FFFF - 16 bits (excluding UTC-16 surrogate code points) case <> of <> when (C >= 16#0800 andalso C =< 16#FFFF) andalso (C < 16#D800 orelse C > 16#DFFF) -> <> = Bin, {C, B, Rest} end; read_codepoint(Bin = <<2#11110:5, B3:3, 2#10:2, B2:6, 2#10:2, B1:6, 2#10:2, B0:6, Rest/binary>>) -> %% U+10000 - U+10FFFF - 21 bits case <> of <> when (C >= 16#010000 andalso C =< 16#10FFFF) -> <> = Bin, {C, B, Rest} end. -spec codepoint_foldl(fun((unichar(), _) -> _), _, binary()) -> _. codepoint_foldl(F, Acc, <<>>) when is_function(F, 2) -> Acc; codepoint_foldl(F, Acc, Bin) -> {C, _, Rest} = read_codepoint(Bin), codepoint_foldl(F, F(C, Acc), Rest). -spec bytes_foldl(fun((binary(), _) -> _), _, binary()) -> _. bytes_foldl(F, Acc, <<>>) when is_function(F, 2) -> Acc; bytes_foldl(F, Acc, Bin) -> {_, B, Rest} = read_codepoint(Bin), bytes_foldl(F, F(B, Acc), Rest). -spec bytes_to_codepoints(binary()) -> [unichar()]. bytes_to_codepoints(B) -> lists:reverse(codepoint_foldl(fun (C, Acc) -> [C | Acc] end, [], B)). -spec len(binary()) -> non_neg_integer(). len(<<>>) -> 0; len(B) -> {_, _, Rest} = read_codepoint(B), 1 + len(Rest). -spec valid_utf8_bytes(B::binary()) -> binary(). %% @doc Return only the bytes in B that represent valid UTF-8. Uses %% the following recursive algorithm: skip one byte if B does not %% follow UTF-8 syntax (a 1-4 byte encoding of some number), %% skip sequence of 2-4 bytes if it represents an overlong encoding %% or bad code point (surrogate U+D800 - U+DFFF or > U+10FFFF). valid_utf8_bytes(B) when is_binary(B) -> binary_skip_bytes(B, invalid_utf8_indexes(B)). %% Internal API -spec binary_skip_bytes(binary(), [non_neg_integer()]) -> binary(). %% @doc Return B, but skipping the 0-based indexes in L. binary_skip_bytes(B, []) -> B; binary_skip_bytes(B, L) -> binary_skip_bytes(B, L, 0, []). %% @private -spec binary_skip_bytes(binary(), [non_neg_integer()], non_neg_integer(), iolist()) -> binary(). binary_skip_bytes(B, [], _N, Acc) -> iolist_to_binary(lists:reverse([B | Acc])); binary_skip_bytes(<<_, RestB/binary>>, [N | RestL], N, Acc) -> binary_skip_bytes(RestB, RestL, 1 + N, Acc); binary_skip_bytes(<>, L, N, Acc) -> binary_skip_bytes(RestB, L, 1 + N, [C | Acc]). -spec invalid_utf8_indexes(binary()) -> [non_neg_integer()]. %% @doc Return the 0-based indexes in B that are not valid UTF-8. invalid_utf8_indexes(B) -> invalid_utf8_indexes(B, 0, []). %% @private. -spec invalid_utf8_indexes(binary(), non_neg_integer(), [non_neg_integer()]) -> [non_neg_integer()]. invalid_utf8_indexes(<>, N, Acc) when C < 16#80 -> %% U+0000 - U+007F - 7 bits invalid_utf8_indexes(Rest, 1 + N, Acc); invalid_utf8_indexes(<>, N, Acc) when C1 band 16#E0 =:= 16#C0, C2 band 16#C0 =:= 16#80 -> %% U+0080 - U+07FF - 11 bits case ((C1 band 16#1F) bsl 6) bor (C2 band 16#3F) of C when C < 16#80 -> %% Overlong encoding. invalid_utf8_indexes(Rest, 2 + N, [1 + N, N | Acc]); _ -> %% Upper bound U+07FF does not need to be checked invalid_utf8_indexes(Rest, 2 + N, Acc) end; invalid_utf8_indexes(<>, N, Acc) when C1 band 16#F0 =:= 16#E0, C2 band 16#C0 =:= 16#80, C3 band 16#C0 =:= 16#80 -> %% U+0800 - U+FFFF - 16 bits case ((((C1 band 16#0F) bsl 6) bor (C2 band 16#3F)) bsl 6) bor (C3 band 16#3F) of C when (C < 16#800) orelse (C >= 16#D800 andalso C =< 16#DFFF) -> %% Overlong encoding or surrogate. invalid_utf8_indexes(Rest, 3 + N, [2 + N, 1 + N, N | Acc]); _ -> %% Upper bound U+FFFF does not need to be checked invalid_utf8_indexes(Rest, 3 + N, Acc) end; invalid_utf8_indexes(<>, N, Acc) when C1 band 16#F8 =:= 16#F0, C2 band 16#C0 =:= 16#80, C3 band 16#C0 =:= 16#80, C4 band 16#C0 =:= 16#80 -> %% U+10000 - U+10FFFF - 21 bits case ((((((C1 band 16#0F) bsl 6) bor (C2 band 16#3F)) bsl 6) bor (C3 band 16#3F)) bsl 6) bor (C4 band 16#3F) of C when (C < 16#10000) orelse (C > 16#10FFFF) -> %% Overlong encoding or invalid code point. invalid_utf8_indexes(Rest, 4 + N, [3 + N, 2 + N, 1 + N, N | Acc]); _ -> invalid_utf8_indexes(Rest, 4 + N, Acc) end; invalid_utf8_indexes(<<_, Rest/binary>>, N, Acc) -> %% Invalid char invalid_utf8_indexes(Rest, 1 + N, [N | Acc]); invalid_utf8_indexes(<<>>, _N, Acc) -> lists:reverse(Acc). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). binary_skip_bytes_test() -> ?assertEqual(<<"foo">>, binary_skip_bytes(<<"foo">>, [])), ?assertEqual(<<"foobar">>, binary_skip_bytes(<<"foo bar">>, [3])), ?assertEqual(<<"foo">>, binary_skip_bytes(<<"foo bar">>, [3, 4, 5, 6])), ?assertEqual(<<"oo bar">>, binary_skip_bytes(<<"foo bar">>, [0])), ok. invalid_utf8_indexes_test() -> ?assertEqual( [], invalid_utf8_indexes(<<"unicode snowman for you: ", 226, 152, 131>>)), ?assertEqual( [0], invalid_utf8_indexes(<<128>>)), ?assertEqual( [57,59,60,64,66,67], invalid_utf8_indexes(<<"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (", 167, 65, 170, 186, 73, 83, 80, 166, 87, 186, 217, 41, 41>>)), ok. codepoint_to_bytes_test() -> %% U+0000 - U+007F - 7 bits %% U+0080 - U+07FF - 11 bits %% U+0800 - U+FFFF - 16 bits (excluding UTC-16 surrogate code points) %% U+10000 - U+10FFFF - 21 bits ?assertEqual( <<"a">>, codepoint_to_bytes($a)), ?assertEqual( <<16#c2, 16#80>>, codepoint_to_bytes(16#80)), ?assertEqual( <<16#df, 16#bf>>, codepoint_to_bytes(16#07ff)), ?assertEqual( <<16#ef, 16#bf, 16#bf>>, codepoint_to_bytes(16#ffff)), ?assertEqual( <<16#f4, 16#8f, 16#bf, 16#bf>>, codepoint_to_bytes(16#10ffff)), ok. bytes_foldl_test() -> ?assertEqual( <<"abc">>, bytes_foldl(fun (B, Acc) -> <> end, <<>>, <<"abc">>)), ?assertEqual( <<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>, bytes_foldl(fun (B, Acc) -> <> end, <<>>, <<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>)), ok. bytes_to_codepoints_test() -> ?assertEqual( "abc" ++ [16#2603, 16#4e2d, 16#85, 16#10ffff], bytes_to_codepoints(<<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>)), ok. codepoint_foldl_test() -> ?assertEqual( "cba", codepoint_foldl(fun (C, Acc) -> [C | Acc] end, [], <<"abc">>)), ?assertEqual( [16#10ffff, 16#85, 16#4e2d, 16#2603 | "cba"], codepoint_foldl(fun (C, Acc) -> [C | Acc] end, [], <<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>)), ok. len_test() -> ?assertEqual( 29, len(<<"unicode snowman for you: ", 226, 152, 131, 228, 184, 173, 194, 133, 244, 143, 191, 191>>)), ok. codepoints_to_bytes_test() -> ?assertEqual( iolist_to_binary(lists:map(fun codepoint_to_bytes/1, lists:seq(1, 1000))), codepoints_to_bytes(lists:seq(1, 1000))), ok. valid_utf8_bytes_test() -> ?assertEqual( <<"invalid U+11ffff: ">>, valid_utf8_bytes(<<"invalid U+11ffff: ", 244, 159, 191, 191>>)), ?assertEqual( <<"U+10ffff: ", 244, 143, 191, 191>>, valid_utf8_bytes(<<"U+10ffff: ", 244, 143, 191, 191>>)), ?assertEqual( <<"overlong 2-byte encoding (a): ">>, valid_utf8_bytes(<<"overlong 2-byte encoding (a): ", 2#11000001, 2#10100001>>)), ?assertEqual( <<"overlong 2-byte encoding (!): ">>, valid_utf8_bytes(<<"overlong 2-byte encoding (!): ", 2#11000000, 2#10100001>>)), ?assertEqual( <<"mu: ", 194, 181>>, valid_utf8_bytes(<<"mu: ", 194, 181>>)), ?assertEqual( <<"bad coding bytes: ">>, valid_utf8_bytes(<<"bad coding bytes: ", 2#10011111, 2#10111111, 2#11111111>>)), ?assertEqual( <<"low surrogate (unpaired): ">>, valid_utf8_bytes(<<"low surrogate (unpaired): ", 237, 176, 128>>)), ?assertEqual( <<"high surrogate (unpaired): ">>, valid_utf8_bytes(<<"high surrogate (unpaired): ", 237, 191, 191>>)), ?assertEqual( <<"unicode snowman for you: ", 226, 152, 131>>, valid_utf8_bytes(<<"unicode snowman for you: ", 226, 152, 131>>)), ?assertEqual( <<"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (AISPW))">>, valid_utf8_bytes(<<"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (", 167, 65, 170, 186, 73, 83, 80, 166, 87, 186, 217, 41, 41>>)), ok. -endif. tsung-1.8.0/src/lib/mochinum.erl0000644000201100017670000002647614377756736016275 0ustar nniclausdream%% @copyright 2007 Mochi Media, Inc. %% @author Bob Ippolito %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Useful numeric algorithms for floats that cover some deficiencies %% in the math module. More interesting is digits/1, which implements %% the algorithm from: %% http://www.cs.indiana.edu/~burger/fp/index.html %% See also "Printing Floating-Point Numbers Quickly and Accurately" %% in Proceedings of the SIGPLAN '96 Conference on Programming Language %% Design and Implementation. -module(mochinum). -author("Bob Ippolito "). -export([digits/1, frexp/1, int_pow/2, int_ceil/1]). %% IEEE 754 Float exponent bias -define(FLOAT_BIAS, 1022). -define(MIN_EXP, -1074). -define(BIG_POW, 4503599627370496). %% External API %% @spec digits(number()) -> string() %% @doc Returns a string that accurately represents the given integer or float %% using a conservative amount of digits. Great for generating %% human-readable output, or compact ASCII serializations for floats. digits(N) when is_integer(N) -> integer_to_list(N); digits(0.0) -> "0.0"; digits(Float) -> {Frac1, Exp1} = frexp_int(Float), [Place0 | Digits0] = digits1(Float, Exp1, Frac1), {Place, Digits} = transform_digits(Place0, Digits0), R = insert_decimal(Place, Digits), case Float < 0 of true -> [$- | R]; _ -> R end. %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()} %% @doc Return the fractional and exponent part of an IEEE 754 double, %% equivalent to the libc function of the same name. %% F = Frac * pow(2, Exp). frexp(F) -> frexp1(unpack(F)). %% @spec int_pow(X::integer(), N::integer()) -> Y::integer() %% @doc Moderately efficient way to exponentiate integers. %% int_pow(10, 2) = 100. int_pow(_X, 0) -> 1; int_pow(X, N) when N > 0 -> int_pow(X, N, 1). %% @spec int_ceil(F::float()) -> integer() %% @doc Return the ceiling of F as an integer. The ceiling is defined as %% F when F == trunc(F); %% trunc(F) when F < 0; %% trunc(F) + 1 when F > 0. int_ceil(X) -> T = trunc(X), case (X - T) of Pos when Pos > 0 -> T + 1; _ -> T end. %% Internal API int_pow(X, N, R) when N < 2 -> R * X; int_pow(X, N, R) -> int_pow(X * X, N bsr 1, case N band 1 of 1 -> R * X; 0 -> R end). insert_decimal(0, S) -> "0." ++ S; insert_decimal(Place, S) when Place > 0 -> L = length(S), case Place - L of 0 -> S ++ ".0"; N when N < 0 -> {S0, S1} = lists:split(L + N, S), S0 ++ "." ++ S1; N when N < 6 -> %% More places than digits S ++ lists:duplicate(N, $0) ++ ".0"; _ -> insert_decimal_exp(Place, S) end; insert_decimal(Place, S) when Place > -6 -> "0." ++ lists:duplicate(abs(Place), $0) ++ S; insert_decimal(Place, S) -> insert_decimal_exp(Place, S). insert_decimal_exp(Place, S) -> [C | S0] = S, S1 = case S0 of [] -> "0"; _ -> S0 end, Exp = case Place < 0 of true -> "e-"; false -> "e+" end, [C] ++ "." ++ S1 ++ Exp ++ integer_to_list(abs(Place - 1)). digits1(Float, Exp, Frac) -> Round = ((Frac band 1) =:= 0), case Exp >= 0 of true -> BExp = 1 bsl Exp, case (Frac =/= ?BIG_POW) of true -> scale((Frac * BExp * 2), 2, BExp, BExp, Round, Round, Float); false -> scale((Frac * BExp * 4), 4, (BExp * 2), BExp, Round, Round, Float) end; false -> case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of true -> scale((Frac * 2), 1 bsl (1 - Exp), 1, 1, Round, Round, Float); false -> scale((Frac * 4), 1 bsl (2 - Exp), 2, 1, Round, Round, Float) end end. scale(R, S, MPlus, MMinus, LowOk, HighOk, Float) -> Est = int_ceil(math:log10(abs(Float)) - 1.0e-10), %% Note that the scheme implementation uses a 326 element look-up table %% for int_pow(10, N) where we do not. case Est >= 0 of true -> fixup(R, S * int_pow(10, Est), MPlus, MMinus, Est, LowOk, HighOk); false -> Scale = int_pow(10, -Est), fixup(R * Scale, S, MPlus * Scale, MMinus * Scale, Est, LowOk, HighOk) end. fixup(R, S, MPlus, MMinus, K, LowOk, HighOk) -> TooLow = case HighOk of true -> (R + MPlus) >= S; false -> (R + MPlus) > S end, case TooLow of true -> [(K + 1) | generate(R, S, MPlus, MMinus, LowOk, HighOk)]; false -> [K | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)] end. generate(R0, S, MPlus, MMinus, LowOk, HighOk) -> D = R0 div S, R = R0 rem S, TC1 = case LowOk of true -> R =< MMinus; false -> R < MMinus end, TC2 = case HighOk of true -> (R + MPlus) >= S; false -> (R + MPlus) > S end, case TC1 of false -> case TC2 of false -> [D | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)]; true -> [D + 1] end; true -> case TC2 of false -> [D]; true -> case R * 2 < S of true -> [D]; false -> [D + 1] end end end. unpack(Float) -> <> = <>, {Sign, Exp, Frac}. frexp1({_Sign, 0, 0}) -> {0.0, 0}; frexp1({Sign, 0, Frac}) -> Exp = log2floor(Frac), <> = <>, {Frac1, -(?FLOAT_BIAS) - 52 + Exp}; frexp1({Sign, Exp, Frac}) -> <> = <>, {Frac1, Exp - ?FLOAT_BIAS}. log2floor(Int) -> log2floor(Int, 0). log2floor(0, N) -> N; log2floor(Int, N) -> log2floor(Int bsr 1, 1 + N). transform_digits(Place, [0 | Rest]) -> transform_digits(Place, Rest); transform_digits(Place, Digits) -> {Place, [$0 + D || D <- Digits]}. frexp_int(F) -> case unpack(F) of {_Sign, 0, Frac} -> {Frac, ?MIN_EXP}; {_Sign, Exp, Frac} -> {Frac + (1 bsl 52), Exp - 53 - ?FLOAT_BIAS} end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). int_ceil_test() -> ?assertEqual(1, int_ceil(0.0001)), ?assertEqual(0, int_ceil(0.0)), ?assertEqual(1, int_ceil(0.99)), ?assertEqual(1, int_ceil(1.0)), ?assertEqual(-1, int_ceil(-1.5)), ?assertEqual(-2, int_ceil(-2.0)), ok. int_pow_test() -> ?assertEqual(1, int_pow(1, 1)), ?assertEqual(1, int_pow(1, 0)), ?assertEqual(1, int_pow(10, 0)), ?assertEqual(10, int_pow(10, 1)), ?assertEqual(100, int_pow(10, 2)), ?assertEqual(1000, int_pow(10, 3)), ok. digits_test() -> ?assertEqual("0", digits(0)), ?assertEqual("0.0", digits(0.0)), ?assertEqual("1.0", digits(1.0)), ?assertEqual("-1.0", digits(-1.0)), ?assertEqual("0.1", digits(0.1)), ?assertEqual("0.01", digits(0.01)), ?assertEqual("0.001", digits(0.001)), ?assertEqual("1.0e+6", digits(1000000.0)), ?assertEqual("0.5", digits(0.5)), ?assertEqual("4503599627370496.0", digits(4503599627370496.0)), %% small denormalized number %% 4.94065645841246544177e-324 =:= 5.0e-324 <> = <<0,0,0,0,0,0,0,1>>, ?assertEqual("5.0e-324", digits(SmallDenorm)), ?assertEqual(SmallDenorm, list_to_float(digits(SmallDenorm))), %% large denormalized number %% 2.22507385850720088902e-308 <> = <<0,15,255,255,255,255,255,255>>, ?assertEqual("2.225073858507201e-308", digits(BigDenorm)), ?assertEqual(BigDenorm, list_to_float(digits(BigDenorm))), %% small normalized number %% 2.22507385850720138309e-308 <> = <<0,16,0,0,0,0,0,0>>, ?assertEqual("2.2250738585072014e-308", digits(SmallNorm)), ?assertEqual(SmallNorm, list_to_float(digits(SmallNorm))), %% large normalized number %% 1.79769313486231570815e+308 <> = <<127,239,255,255,255,255,255,255>>, ?assertEqual("1.7976931348623157e+308", digits(LargeNorm)), ?assertEqual(LargeNorm, list_to_float(digits(LargeNorm))), %% issue #10 - mochinum:frexp(math:pow(2, -1074)). ?assertEqual("5.0e-324", digits(math:pow(2, -1074))), ok. frexp_test() -> %% zero ?assertEqual({0.0, 0}, frexp(0.0)), %% one ?assertEqual({0.5, 1}, frexp(1.0)), %% negative one ?assertEqual({-0.5, 1}, frexp(-1.0)), %% small denormalized number %% 4.94065645841246544177e-324 <> = <<0,0,0,0,0,0,0,1>>, ?assertEqual({0.5, -1073}, frexp(SmallDenorm)), %% large denormalized number %% 2.22507385850720088902e-308 <> = <<0,15,255,255,255,255,255,255>>, ?assertEqual( {0.99999999999999978, -1022}, frexp(BigDenorm)), %% small normalized number %% 2.22507385850720138309e-308 <> = <<0,16,0,0,0,0,0,0>>, ?assertEqual({0.5, -1021}, frexp(SmallNorm)), %% large normalized number %% 1.79769313486231570815e+308 <> = <<127,239,255,255,255,255,255,255>>, ?assertEqual( {0.99999999999999989, 1024}, frexp(LargeNorm)), %% issue #10 - mochinum:frexp(math:pow(2, -1074)). ?assertEqual( {0.5, -1073}, frexp(math:pow(2, -1074))), ok. -endif. tsung-1.8.0/src/lib/mochijson2.erl0000644000201100017670000007674014377756736016530 0ustar nniclausdream%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works %% with binaries as strings, arrays as lists (without an {array, _}) %% wrapper and it only knows how to decode UTF-8 (and ASCII). %% %% JSON terms are decoded as follows (javascript -> erlang): %%
    %%
  • {"key": "value"} -> %% {struct, [{<<"key">>, <<"value">>}]}
  • %%
  • ["array", 123, 12.34, true, false, null] -> %% [<<"array">>, 123, 12.34, true, false, null] %%
  • %%
%%
    %%
  • Strings in JSON decode to UTF-8 binaries in Erlang
  • %%
  • Objects decode to {struct, PropList}
  • %%
  • Numbers decode to integer or float
  • %%
  • true, false, null decode to their respective terms.
  • %%
%% The encoder will accept the same format that the decoder will produce, %% but will also allow additional cases for leniency: %%
    %%
  • atoms other than true, false, null will be considered UTF-8 %% strings (even as a proplist key) %%
  • %%
  • {json, IoList} will insert IoList directly into the output %% with no validation %%
  • %%
  • {array, Array} will be encoded as Array %% (legacy mochijson style) %%
  • %%
  • A non-empty raw proplist will be encoded as an object as long %% as the first pair does not have an atom key of json, struct, %% or array %%
  • %%
-module(mochijson2). -author('bob@mochimedia.com'). -export([encoder/1, encode/1]). -export([decoder/1, decode/1, decode/2]). %% This is a macro to placate syntax highlighters.. -define(Q, $\"). -define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset, column=N+S#decoder.column}). -define(INC_COL(S), S#decoder{offset=1+S#decoder.offset, column=1+S#decoder.column}). -define(INC_LINE(S), S#decoder{offset=1+S#decoder.offset, column=1, line=1+S#decoder.line}). -define(INC_CHAR(S, C), case C of $\n -> S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}; _ -> S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset} end). -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). %% @type json_string() = atom | binary() %% @type json_number() = integer() | float() %% @type json_array() = [json_term()] %% @type json_object() = {struct, [{json_string(), json_term()}]} %% @type json_eep18_object() = {[{json_string(), json_term()}]} %% @type json_iolist() = {json, iolist()} %% @type json_term() = json_string() | json_number() | json_array() | %% json_object() | json_eep18_object() | json_iolist() -record(encoder, {handler=null, utf8=false}). -record(decoder, {object_hook=null, offset=0, line=1, column=1, state=null}). %% @spec encoder([encoder_option()]) -> function() %% @doc Create an encoder/1 with the given options. %% @type encoder_option() = handler_option() | utf8_option() %% @type utf8_option() = boolean(). Emit unicode as utf8 (default - false) encoder(Options) -> State = parse_encoder_options(Options, #encoder{}), fun (O) -> json_encode(O, State) end. %% @spec encode(json_term()) -> iolist() %% @doc Encode the given as JSON to an iolist. encode(Any) -> json_encode(Any, #encoder{}). %% @spec decoder([decoder_option()]) -> function() %% @doc Create a decoder/1 with the given options. decoder(Options) -> State = parse_decoder_options(Options, #decoder{}), fun (O) -> json_decode(O, State) end. %% @spec decode(iolist(), [{format, proplist | eep18 | struct}]) -> json_term() %% @doc Decode the given iolist to Erlang terms using the given object format %% for decoding, where proplist returns JSON objects as [{binary(), json_term()}] %% proplists, eep18 returns JSON objects as {[binary(), json_term()]}, and struct %% returns them as-is. decode(S, Options) -> json_decode(S, parse_decoder_options(Options, #decoder{})). %% @spec decode(iolist()) -> json_term() %% @doc Decode the given iolist to Erlang terms. decode(S) -> json_decode(S, #decoder{}). %% Internal API parse_encoder_options([], State) -> State; parse_encoder_options([{handler, Handler} | Rest], State) -> parse_encoder_options(Rest, State#encoder{handler=Handler}); parse_encoder_options([{utf8, Switch} | Rest], State) -> parse_encoder_options(Rest, State#encoder{utf8=Switch}). parse_decoder_options([], State) -> State; parse_decoder_options([{object_hook, Hook} | Rest], State) -> parse_decoder_options(Rest, State#decoder{object_hook=Hook}); parse_decoder_options([{format, Format} | Rest], State) when Format =:= struct orelse Format =:= eep18 orelse Format =:= proplist -> parse_decoder_options(Rest, State#decoder{object_hook=Format}). json_encode(true, _State) -> <<"true">>; json_encode(false, _State) -> <<"false">>; json_encode(null, _State) -> <<"null">>; json_encode(I, _State) when is_integer(I) -> integer_to_list(I); json_encode(F, _State) when is_float(F) -> mochinum:digits(F); json_encode(S, State) when is_binary(S); is_atom(S) -> json_encode_string(S, State); json_encode([{K, _}|_] = Props, State) when (K =/= struct andalso K =/= array andalso K =/= json) -> json_encode_proplist(Props, State); json_encode({struct, Props}, State) when is_list(Props) -> json_encode_proplist(Props, State); json_encode({Props}, State) when is_list(Props) -> json_encode_proplist(Props, State); json_encode({}, State) -> json_encode_proplist([], State); json_encode(Array, State) when is_list(Array) -> json_encode_array(Array, State); json_encode({array, Array}, State) when is_list(Array) -> json_encode_array(Array, State); json_encode({json, IoList}, _State) -> IoList; json_encode(Bad, #encoder{handler=null}) -> exit({json_encode, {bad_term, Bad}}); json_encode(Bad, State=#encoder{handler=Handler}) -> json_encode(Handler(Bad), State). json_encode_array([], _State) -> <<"[]">>; json_encode_array(L, State) -> F = fun (O, Acc) -> [$,, json_encode(O, State) | Acc] end, [$, | Acc1] = lists:foldl(F, "[", L), lists:reverse([$\] | Acc1]). json_encode_proplist([], _State) -> <<"{}">>; json_encode_proplist(Props, State) -> F = fun ({K, V}, Acc) -> KS = json_encode_string(K, State), VS = json_encode(V, State), [$,, VS, $:, KS | Acc] end, [$, | Acc1] = lists:foldl(F, "{", Props), lists:reverse([$\} | Acc1]). json_encode_string(A, State) when is_atom(A) -> L = atom_to_list(A), case json_string_is_safe(L) of true -> [?Q, L, ?Q]; false -> json_encode_string_unicode(xmerl_ucs:from_utf8(L), State, [?Q]) end; json_encode_string(B, State) when is_binary(B) -> case json_bin_is_safe(B) of true -> [?Q, B, ?Q]; false -> json_encode_string_unicode(xmerl_ucs:from_utf8(B), State, [?Q]) end; json_encode_string(I, _State) when is_integer(I) -> [?Q, integer_to_list(I), ?Q]; json_encode_string(L, State) when is_list(L) -> case json_string_is_safe(L) of true -> [?Q, L, ?Q]; false -> json_encode_string_unicode(L, State, [?Q]) end. json_string_is_safe([]) -> true; json_string_is_safe([C | Rest]) -> case C of ?Q -> false; $\\ -> false; $\b -> false; $\f -> false; $\n -> false; $\r -> false; $\t -> false; C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> false; C when C < 16#7f -> json_string_is_safe(Rest); _ -> false end. json_bin_is_safe(<<>>) -> true; json_bin_is_safe(<>) -> case C of ?Q -> false; $\\ -> false; $\b -> false; $\f -> false; $\n -> false; $\r -> false; $\t -> false; C when C >= 0, C < $\s; C >= 16#7f -> false; C when C < 16#7f -> json_bin_is_safe(Rest) end. json_encode_string_unicode([], _State, Acc) -> lists:reverse([$\" | Acc]); json_encode_string_unicode([C | Cs], State, Acc) -> Acc1 = case C of ?Q -> [?Q, $\\ | Acc]; %% Escaping solidus is only useful when trying to protect %% against "" injection attacks which are only %% possible when JSON is inserted into a HTML document %% in-line. mochijson2 does not protect you from this, so %% if you do insert directly into HTML then you need to %% uncomment the following case or escape the output of encode. %% %% $/ -> %% [$/, $\\ | Acc]; %% $\\ -> [$\\, $\\ | Acc]; $\b -> [$b, $\\ | Acc]; $\f -> [$f, $\\ | Acc]; $\n -> [$n, $\\ | Acc]; $\r -> [$r, $\\ | Acc]; $\t -> [$t, $\\ | Acc]; C when C >= 0, C < $\s -> [unihex(C) | Acc]; C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 -> [xmerl_ucs:to_utf8(C) | Acc]; C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 -> [unihex(C) | Acc]; C when C < 16#7f -> [C | Acc]; _ -> exit({json_encode, {bad_char, C}}) end, json_encode_string_unicode(Cs, State, Acc1). hexdigit(C) when C >= 0, C =< 9 -> C + $0; hexdigit(C) when C =< 15 -> C + $a - 10. unihex(C) when C < 16#10000 -> <> = <>, Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]], [$\\, $u | Digits]; unihex(C) when C =< 16#10FFFF -> N = C - 16#10000, S1 = 16#d800 bor ((N bsr 10) band 16#3ff), S2 = 16#dc00 bor (N band 16#3ff), [unihex(S1), unihex(S2)]. json_decode(L, S) when is_list(L) -> json_decode(iolist_to_binary(L), S); json_decode(B, S) -> {Res, S1} = decode1(B, S), {eof, _} = tokenize(B, S1#decoder{state=trim}), Res. decode1(B, S=#decoder{state=null}) -> case tokenize(B, S#decoder{state=any}) of {{const, C}, S1} -> {C, S1}; {start_array, S1} -> decode_array(B, S1); {start_object, S1} -> decode_object(B, S1) end. make_object(V, #decoder{object_hook=N}) when N =:= null orelse N =:= struct -> V; make_object({struct, P}, #decoder{object_hook=eep18}) -> {P}; make_object({struct, P}, #decoder{object_hook=proplist}) -> P; make_object(V, #decoder{object_hook=Hook}) -> Hook(V). decode_object(B, S) -> decode_object(B, S#decoder{state=key}, []). decode_object(B, S=#decoder{state=key}, Acc) -> case tokenize(B, S) of {end_object, S1} -> V = make_object({struct, lists:reverse(Acc)}, S1), {V, S1#decoder{state=null}}; {{const, K}, S1} -> {colon, S2} = tokenize(B, S1), {V, S3} = decode1(B, S2#decoder{state=null}), decode_object(B, S3#decoder{state=comma}, [{K, V} | Acc]) end; decode_object(B, S=#decoder{state=comma}, Acc) -> case tokenize(B, S) of {end_object, S1} -> V = make_object({struct, lists:reverse(Acc)}, S1), {V, S1#decoder{state=null}}; {comma, S1} -> decode_object(B, S1#decoder{state=key}, Acc) end. decode_array(B, S) -> decode_array(B, S#decoder{state=any}, []). decode_array(B, S=#decoder{state=any}, Acc) -> case tokenize(B, S) of {end_array, S1} -> {lists:reverse(Acc), S1#decoder{state=null}}; {start_array, S1} -> {Array, S2} = decode_array(B, S1), decode_array(B, S2#decoder{state=comma}, [Array | Acc]); {start_object, S1} -> {Array, S2} = decode_object(B, S1), decode_array(B, S2#decoder{state=comma}, [Array | Acc]); {{const, Const}, S1} -> decode_array(B, S1#decoder{state=comma}, [Const | Acc]) end; decode_array(B, S=#decoder{state=comma}, Acc) -> case tokenize(B, S) of {end_array, S1} -> {lists:reverse(Acc), S1#decoder{state=null}}; {comma, S1} -> decode_array(B, S1#decoder{state=any}, Acc) end. tokenize_string(B, S=#decoder{offset=O}) -> case tokenize_string_fast(B, O) of {escape, O1} -> Length = O1 - O, S1 = ?ADV_COL(S, Length), <<_:O/binary, Head:Length/binary, _/binary>> = B, tokenize_string(B, S1, lists:reverse(binary_to_list(Head))); O1 -> Length = O1 - O, <<_:O/binary, String:Length/binary, ?Q, _/binary>> = B, {{const, String}, ?ADV_COL(S, Length + 1)} end. tokenize_string_fast(B, O) -> case B of <<_:O/binary, ?Q, _/binary>> -> O; <<_:O/binary, $\\, _/binary>> -> {escape, O}; <<_:O/binary, C1, _/binary>> when C1 < 128 -> tokenize_string_fast(B, 1 + O); <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, C2 >= 128, C2 =< 191 -> tokenize_string_fast(B, 2 + O); <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, C2 >= 128, C2 =< 191, C3 >= 128, C3 =< 191 -> tokenize_string_fast(B, 3 + O); <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244, C2 >= 128, C2 =< 191, C3 >= 128, C3 =< 191, C4 >= 128, C4 =< 191 -> tokenize_string_fast(B, 4 + O); _ -> throw(invalid_utf8) end. tokenize_string(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, ?Q, _/binary>> -> {{const, iolist_to_binary(lists:reverse(Acc))}, ?INC_COL(S)}; <<_:O/binary, "\\\"", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\" | Acc]); <<_:O/binary, "\\\\", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\\ | Acc]); <<_:O/binary, "\\/", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$/ | Acc]); <<_:O/binary, "\\b", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\b | Acc]); <<_:O/binary, "\\f", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\f | Acc]); <<_:O/binary, "\\n", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\n | Acc]); <<_:O/binary, "\\r", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\r | Acc]); <<_:O/binary, "\\t", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\t | Acc]); <<_:O/binary, "\\u", C3, C2, C1, C0, Rest/binary>> -> C = erlang:list_to_integer([C3, C2, C1, C0], 16), if C > 16#D7FF, C < 16#DC00 -> %% coalesce UTF-16 surrogate pair <<"\\u", D3, D2, D1, D0, _/binary>> = Rest, D = erlang:list_to_integer([D3,D2,D1,D0], 16), [CodePoint] = xmerl_ucs:from_utf16be(<>), Acc1 = lists:reverse(xmerl_ucs:to_utf8(CodePoint), Acc), tokenize_string(B, ?ADV_COL(S, 12), Acc1); true -> Acc1 = lists:reverse(xmerl_ucs:to_utf8(C), Acc), tokenize_string(B, ?ADV_COL(S, 6), Acc1) end; <<_:O/binary, C1, _/binary>> when C1 < 128 -> tokenize_string(B, ?INC_CHAR(S, C1), [C1 | Acc]); <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, C2 >= 128, C2 =< 191 -> tokenize_string(B, ?ADV_COL(S, 2), [C2, C1 | Acc]); <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, C2 >= 128, C2 =< 191, C3 >= 128, C3 =< 191 -> tokenize_string(B, ?ADV_COL(S, 3), [C3, C2, C1 | Acc]); <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244, C2 >= 128, C2 =< 191, C3 >= 128, C3 =< 191, C4 >= 128, C4 =< 191 -> tokenize_string(B, ?ADV_COL(S, 4), [C4, C3, C2, C1 | Acc]); _ -> throw(invalid_utf8) end. tokenize_number(B, S) -> case tokenize_number(B, sign, S, []) of {{int, Int}, S1} -> {{const, list_to_integer(Int)}, S1}; {{float, Float}, S1} -> {{const, list_to_float(Float)}, S1} end. tokenize_number(B, sign, S=#decoder{offset=O}, []) -> case B of <<_:O/binary, $-, _/binary>> -> tokenize_number(B, int, ?INC_COL(S), [$-]); _ -> tokenize_number(B, int, S, []) end; tokenize_number(B, int, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, $0, _/binary>> -> tokenize_number(B, frac, ?INC_COL(S), [$0 | Acc]); <<_:O/binary, C, _/binary>> when C >= $1 andalso C =< $9 -> tokenize_number(B, int1, ?INC_COL(S), [C | Acc]) end; tokenize_number(B, int1, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, int1, ?INC_COL(S), [C | Acc]); _ -> tokenize_number(B, frac, S, Acc) end; tokenize_number(B, frac, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, $., C, _/binary>> when C >= $0, C =< $9 -> tokenize_number(B, frac1, ?ADV_COL(S, 2), [C, $. | Acc]); <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> tokenize_number(B, esign, ?INC_COL(S), [$e, $0, $. | Acc]); _ -> {{int, lists:reverse(Acc)}, S} end; tokenize_number(B, frac1, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, frac1, ?INC_COL(S), [C | Acc]); <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> tokenize_number(B, esign, ?INC_COL(S), [$e | Acc]); _ -> {{float, lists:reverse(Acc)}, S} end; tokenize_number(B, esign, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C =:= $- orelse C=:= $+ -> tokenize_number(B, eint, ?INC_COL(S), [C | Acc]); _ -> tokenize_number(B, eint, S, Acc) end; tokenize_number(B, eint, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]) end; tokenize_number(B, eint1, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]); _ -> {{float, lists:reverse(Acc)}, S} end. tokenize(B, S=#decoder{offset=O}) -> case B of <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) -> tokenize(B, ?INC_CHAR(S, C)); <<_:O/binary, "{", _/binary>> -> {start_object, ?INC_COL(S)}; <<_:O/binary, "}", _/binary>> -> {end_object, ?INC_COL(S)}; <<_:O/binary, "[", _/binary>> -> {start_array, ?INC_COL(S)}; <<_:O/binary, "]", _/binary>> -> {end_array, ?INC_COL(S)}; <<_:O/binary, ",", _/binary>> -> {comma, ?INC_COL(S)}; <<_:O/binary, ":", _/binary>> -> {colon, ?INC_COL(S)}; <<_:O/binary, "null", _/binary>> -> {{const, null}, ?ADV_COL(S, 4)}; <<_:O/binary, "true", _/binary>> -> {{const, true}, ?ADV_COL(S, 4)}; <<_:O/binary, "false", _/binary>> -> {{const, false}, ?ADV_COL(S, 5)}; <<_:O/binary, "\"", _/binary>> -> tokenize_string(B, ?INC_COL(S)); <<_:O/binary, C, _/binary>> when (C >= $0 andalso C =< $9) orelse C =:= $- -> tokenize_number(B, S); <<_:O/binary>> -> trim = S#decoder.state, {eof, S} end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). %% testing constructs borrowed from the Yaws JSON implementation. %% Create an object from a list of Key/Value pairs. obj_new() -> {struct, []}. is_obj({struct, Props}) -> F = fun ({K, _}) when is_binary(K) -> true end, lists:all(F, Props). obj_from_list(Props) -> Obj = {struct, Props}, ?assert(is_obj(Obj)), Obj. %% Test for equivalence of Erlang terms. %% Due to arbitrary order of construction, equivalent objects might %% compare unequal as erlang terms, so we need to carefully recurse %% through aggregates (tuples and objects). equiv({struct, Props1}, {struct, Props2}) -> equiv_object(Props1, Props2); equiv(L1, L2) when is_list(L1), is_list(L2) -> equiv_list(L1, L2); equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2; equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true. %% Object representation and traversal order is unknown. %% Use the sledgehammer and sort property lists. equiv_object(Props1, Props2) -> L1 = lists:keysort(1, Props1), L2 = lists:keysort(1, Props2), Pairs = lists:zip(L1, L2), true = lists:all(fun({{K1, V1}, {K2, V2}}) -> equiv(K1, K2) and equiv(V1, V2) end, Pairs). %% Recursively compare tuple elements for equivalence. equiv_list([], []) -> true; equiv_list([V1 | L1], [V2 | L2]) -> equiv(V1, V2) andalso equiv_list(L1, L2). decode_test() -> [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>), <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]). e2j_vec_test() -> test_one(e2j_test_vec(utf8), 1). test_one([], _N) -> %% io:format("~p tests passed~n", [N-1]), ok; test_one([{E, J} | Rest], N) -> %% io:format("[~p] ~p ~p~n", [N, E, J]), true = equiv(E, decode(J)), true = equiv(E, decode(encode(E))), test_one(Rest, 1+N). e2j_test_vec(utf8) -> [ {1, "1"}, {3.1416, "3.14160"}, %% text representation may truncate, trail zeroes {-1, "-1"}, {-3.1416, "-3.14160"}, {12.0e10, "1.20000e+11"}, {1.234E+10, "1.23400e+10"}, {-1.234E-10, "-1.23400e-10"}, {10.0, "1.0e+01"}, {123.456, "1.23456E+2"}, {10.0, "1e1"}, {<<"foo">>, "\"foo\""}, {<<"foo", 5, "bar">>, "\"foo\\u0005bar\""}, {<<"">>, "\"\""}, {<<"\n\n\n">>, "\"\\n\\n\\n\""}, {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\""}, {obj_new(), "{}"}, {obj_from_list([{<<"foo">>, <<"bar">>}]), "{\"foo\":\"bar\"}"}, {obj_from_list([{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]), "{\"foo\":\"bar\",\"baz\":123}"}, {[], "[]"}, {[[]], "[[]]"}, {[1, <<"foo">>], "[1,\"foo\"]"}, %% json array in a json object {obj_from_list([{<<"foo">>, [123]}]), "{\"foo\":[123]}"}, %% json object in a json object {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]), "{\"foo\":{\"bar\":true}}"}, %% fold evaluation order {obj_from_list([{<<"foo">>, []}, {<<"bar">>, obj_from_list([{<<"baz">>, true}])}, {<<"alice">>, <<"bob">>}]), "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, %% json object in a json array {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null], "[-123,\"foo\",{\"bar\":[]},null]"} ]. %% test utf8 encoding encoder_utf8_test() -> %% safe conversion case (default) [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] = encode(<<1,"\321\202\320\265\321\201\321\202">>), %% raw utf8 output (optional) Enc = mochijson2:encoder([{utf8, true}]), [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] = Enc(<<1,"\321\202\320\265\321\201\321\202">>). input_validation_test() -> Good = [ {16#00A3, <>}, %% pound {16#20AC, <>}, %% euro {16#10196, <>} %% denarius ], lists:foreach(fun({CodePoint, UTF8}) -> Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)), Expect = decode(UTF8) end, Good), Bad = [ %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte <>, %% missing continuations, last byte in each should be 80-BF <>, <>, <>, %% we don't support code points > 10FFFF per RFC 3629 <>, %% escape characters trigger a different code path <> ], lists:foreach( fun(X) -> ok = try decode(X) catch invalid_utf8 -> ok end, %% could be {ucs,{bad_utf8_character_code}} or %% {json_encode,{bad_char,_}} {'EXIT', _} = (catch encode(X)) end, Bad). inline_json_test() -> ?assertEqual(<<"\"iodata iodata\"">>, iolist_to_binary( encode({json, [<<"\"iodata">>, " iodata\""]}))), ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]}, decode( encode({struct, [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))), ok. big_unicode_test() -> UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)), ?assertEqual( <<"\"\\ud834\\udd20\"">>, iolist_to_binary(encode(UTF8Seq))), ?assertEqual( UTF8Seq, decode(iolist_to_binary(encode(UTF8Seq)))), ok. custom_decoder_test() -> ?assertEqual( {struct, [{<<"key">>, <<"value">>}]}, (decoder([]))("{\"key\": \"value\"}")), F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end, ?assertEqual( win, (decoder([{object_hook, F}]))("{\"key\": \"value\"}")), ok. atom_test() -> %% JSON native atoms [begin ?assertEqual(A, decode(atom_to_list(A))), ?assertEqual(iolist_to_binary(atom_to_list(A)), iolist_to_binary(encode(A))) end || A <- [true, false, null]], %% Atom to string ?assertEqual( <<"\"foo\"">>, iolist_to_binary(encode(foo))), ?assertEqual( <<"\"\\ud834\\udd20\"">>, iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))), ok. key_encode_test() -> %% Some forms are accepted as keys that would not be strings in other %% cases ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode({struct, [{foo, 1}]}))), ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))), ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode({struct, [{"foo", 1}]}))), ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode([{foo, 1}]))), ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode([{<<"foo">>, 1}]))), ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode([{"foo", 1}]))), ?assertEqual( <<"{\"\\ud834\\udd20\":1}">>, iolist_to_binary( encode({struct, [{[16#0001d120], 1}]}))), ?assertEqual( <<"{\"1\":1}">>, iolist_to_binary(encode({struct, [{1, 1}]}))), ok. unsafe_chars_test() -> Chars = "\"\\\b\f\n\r\t", [begin ?assertEqual(false, json_string_is_safe([C])), ?assertEqual(false, json_bin_is_safe(<>)), ?assertEqual(<>, decode(encode(<>))) end || C <- Chars], ?assertEqual( false, json_string_is_safe([16#0001d120])), ?assertEqual( false, json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))), ?assertEqual( [16#0001d120], xmerl_ucs:from_utf8( binary_to_list( decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))), ?assertEqual( false, json_string_is_safe([16#110000])), ?assertEqual( false, json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))), %% solidus can be escaped but isn't unsafe by default ?assertEqual( <<"/">>, decode(<<"\"\\/\"">>)), ok. int_test() -> ?assertEqual(0, decode("0")), ?assertEqual(1, decode("1")), ?assertEqual(11, decode("11")), ok. large_int_test() -> ?assertEqual(<<"-2147483649214748364921474836492147483649">>, iolist_to_binary(encode(-2147483649214748364921474836492147483649))), ?assertEqual(<<"2147483649214748364921474836492147483649">>, iolist_to_binary(encode(2147483649214748364921474836492147483649))), ok. float_test() -> ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649.0))), ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648.0))), ok. handler_test() -> ?assertEqual( {'EXIT',{json_encode,{bad_term,{x,y}}}}, catch encode({x,y})), F = fun ({x,y}) -> [] end, ?assertEqual( <<"[]">>, iolist_to_binary((encoder([{handler, F}]))({x, y}))), ok. encode_empty_test_() -> [{A, ?_assertEqual(<<"{}">>, iolist_to_binary(encode(B)))} || {A, B} <- [{"eep18 {}", {}}, {"eep18 {[]}", {[]}}, {"{struct, []}", {struct, []}}]]. encode_test_() -> P = [{<<"k">>, <<"v">>}], JSON = iolist_to_binary(encode({struct, P})), [{atom_to_list(F), ?_assertEqual(JSON, iolist_to_binary(encode(decode(JSON, [{format, F}]))))} || F <- [struct, eep18, proplist]]. format_test_() -> P = [{<<"k">>, <<"v">>}], JSON = iolist_to_binary(encode({struct, P})), [{atom_to_list(F), ?_assertEqual(A, decode(JSON, [{format, F}]))} || {F, A} <- [{struct, {struct, P}}, {eep18, {P}}, {proplist, P}]]. -endif. tsung-1.8.0/src/tsung/0000755000201100017670000000000014377757020014310 5ustar nniclausdreamtsung-1.8.0/src/tsung/tsung.app.in0000644000201100017670000001125414377756736016577 0ustar nniclausdream{application, tsung, [{description, "tsung, a load testing tool for TCP/UDP servers"}, {vsn, "@PACKAGE_VERSION@"}, {modules, [ gen_ts_transport, mochijson2, mochinum, mochiutf8, mochiweb_charref, mochiweb_headers, mochiweb_html, mochiweb_util, mochiweb_xpath, mochiweb_xpath_functions, mochiweb_xpath_parser, mochiweb_xpath_utils, mqtt_frame, oauth, oauth_hmac_sha1, oauth_http, oauth_plaintext, oauth_rsa_sha1, oauth_unix, oauth_uri, pgsql_proto, pgsql_util, rabbit_binary_generator, rabbit_binary_parser, rabbit_command_assembler, rabbit_framing_amqp_0_9_1, rabbit_misc, rfc4515_parser, ts_amqp, ts_bosh, ts_bosh_ssl, ts_client, ts_client_sup, ts_cport, ts_digest, ts_dynvars, ts_erlang, ts_fs, ts_http, ts_http_common, ts_ip_scan, ts_jabber, ts_jabber_common, ts_job, ts_launcher, ts_launcher_mgr, ts_launcher_static, ts_ldap, ts_ldap_common, ts_local_mon, ts_mon_cache, ts_mqtt, ts_mysql, ts_pgsql, ts_plugin, ts_raw, ts_reports, ts_search, ts_server_websocket, ts_session_cache, ts_shell, ts_ssl, ts_ssl6, ts_ssl_session_cache, ts_stats, ts_sup, ts_tcp, ts_tcp6, ts_udp, ts_udp6, tsung, ts_utils, ts_webdav, ts_websocket, uuid, websocket ]}, {registered, [ ts_launcher, ts_launcher_static, ts_mon_cache, ts_sup, ts_session_cache ]}, {env, [ {debug_level, 2}, {snd_size, 32768}, % send buffer size {rcv_size, 32768}, % receive buffer size {idle_timeout, 600000}, % 10min timeout {global_ack_timeout, infinity}, % global ack timeout {connect_timeout, 30000}, {max_warm_delay, 15000}, {dump, full}, % full or light {parse_type, noparse}, {persistent, true}, % persistent connection: true or false {mes_type, dynamic}, % dynamic or static {nclients, 10}, % number of client to connect {log_file, "./tsung.log"}, % log file name %% use for IMS GET : {http_modified_since_date, "Fri, 14 Nov 2003 02:43:31 GMT"}, {client_retry_timeout, 10}, % retry sending (in microsec.) {max_retries, 3}, % number of max retries {ssl_ciphers, negotiate}, {ssl_versions, negotiate}, %%% -------- JABBER OPTIONS {jabber_users, 2000000}, {jabber_username, "c"}, {jabber_password, "pas"}, {jabber_domain, "mydomain.com"}, %%% -------- WEBSOCKET OPTIONS {websocket_path, "/chat"} ]}, {applications, [ @ERLANG_APPLICATIONS@]}, {mod, {tsung, []}} ]}. tsung-1.8.0/src/tsung/ts_websocket.erl0000644000201100017670000001770414377756736017536 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_websocket). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -behavior(ts_plugin). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_websocket.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, parse/2, dump/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session (persistent & bidirectional) %% Returns: {ok, true|false, true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true, true}. %% @spec decode_buffer(Buffer::binary(),Session::record(jabber)) -> %% NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#websocket_session{}) -> case websocket:decode(Buffer) of more -> <<>>; {_Opcode, Payload, _Rest} -> Payload end. %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #websocket_session{}. dump(A,B) -> ts_plugin:dump(A,B). %%---------------------------------------------------------------------- %% Function: get_message/1 %% Purpose: Build a message/request , %% Args: record %% Returns: binary %%---------------------------------------------------------------------- get_message(#websocket_request{type = connect, path = Path, subprotos = SubProtocol, version = Version, headers = Headers, origin = Origin}, State=#state_rcv{session = WebsocketSession}) -> {Request, Accept} = websocket:get_handshake(State#state_rcv.host, Path, SubProtocol, Version, Origin, Headers), {Request, WebsocketSession#websocket_session{status = waiting_handshake, accept = Accept}}; get_message(#websocket_request{type = message, data = Data, frame = Frame}, #state_rcv{session = WebsocketSession}) when WebsocketSession#websocket_session.status == connected -> ResultData = case Frame of "text" -> websocket:encode_text(list_to_binary(Data)); _ -> websocket:encode_binary(list_to_binary(Data)) end, {ResultData, WebsocketSession}; get_message(#websocket_request{type = close}, #state_rcv{session = WebsocketSession}) when WebsocketSession#websocket_session.status == connected -> {websocket:encode_close(<<"close">>), WebsocketSession}. %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: parse the response from the server and keep information %% about the response in State#state_rcv.session %% Args: Data (binary), State (#state_rcv) %% Returns: {NewState, Options for socket (list), Close = true|false} %%---------------------------------------------------------------------- parse(closed, State) -> {State#state_rcv{ack_done = true, acc = [], datasize=0}, [], true}; %% new response, compute data size (for stats) parse(Data, State=#state_rcv{acc = [], datasize= 0}) -> parse(Data, State#state_rcv{datasize= size(Data)}); %% handshake stage, parse response, and validate parse(Data, State=#state_rcv{acc = [], session = WebsocketSession}) when WebsocketSession#websocket_session.status == waiting_handshake -> Acc = list_to_binary(State#state_rcv.acc), Header = <>, Accept = WebsocketSession#websocket_session.accept, case websocket:check_handshake(Header, Accept) of ok -> ?Debug("handshake success:~n"), ts_mon_cache:add({count, websocket_succ}), {State#state_rcv{ack_done = true, session = WebsocketSession#websocket_session{ status = connected}}, [], false}; {error, _Reason} -> ?DebugF("handshake fail: ~p~n", [_Reason]), ts_mon_cache:add({count, websocket_fail}), {State#state_rcv{ack_done = true}, [], true} end; %% normal websocket message parse(Data, State=#state_rcv{acc = [], session = WebsocketSession}) when WebsocketSession#websocket_session.status == connected -> case websocket:decode(Data) of {?OP_CLOSE, _Reason, _} -> ?DebugF("receive close from server: ~p~n", [_Reason]), {State#state_rcv{ack_done = true}, [], true}; {_Opcode, _Payload, Left} -> ?DebugF("receive from server: ~p ~p~n", [_Opcode, _Payload]), {State#state_rcv{ack_done = true, acc = Left}, [], false}; more -> ?DebugF("receive incomplete frame from server: ~p~n", [Data]), {State#state_rcv{ack_done = false, acc = Data}, [], false} end; %% more data, add this to accumulator and parse, update datasize parse(Data, State=#state_rcv{acc = Acc, datasize = DataSize}) -> NewSize= DataSize + size(Data), parse(<< Acc/binary, Data/binary >>, State#state_rcv{acc = [], datasize = NewSize}). parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data, State). %%---------------------------------------------------------------------- %% Function: parse_config/2 %% Purpose: parse tags in the XML config file related to the protocol %% Returns: List %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_websocket:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: we dont actually do anything %% Returns: #websocket_request %%---------------------------------------------------------------------- add_dynparams(true, {DynVars, _S}, Param = #websocket_request{type = message, data = Data}, _HostData) -> NewData = ts_search:subst(Data, DynVars), Param#websocket_request{data = NewData}; add_dynparams(true, {DynVars, _S}, Param = #websocket_request{type = connect, path = Path, headers = Headers}, _HostData) -> NewPath = ts_search:subst(Path, DynVars), NewHeaders = lists:foldl(fun ({Name, Value}, Result) -> [{Name, ts_search:subst(Value, DynVars)} | Result] end, [], Headers), Param#websocket_request{path = NewPath, headers = NewHeaders}; add_dynparams(_Bool, _DynData, Param, _HostData) -> Param#websocket_request{}. tsung-1.8.0/src/tsung/ts_webdav.erl0000644000201100017670000000661014377756736017012 0ustar nniclausdream%%% %%% Copyright © Nicolas Niclausse. 2008 %%% %%% Author : Nicolas Niclausse %%% Created: 12 mar 2008 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_webdav). -vc('$Id: ts_webdav.erl,v 0.0 2008/03/12 12:47:07 nniclaus Exp $ '). -author('nicolas.niclausse@niclux.org'). -behaviour(ts_plugin). -include("ts_profile.hrl"). -include("ts_http.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, parse/2, parse_bidi/2, dump/2, parse_config/2, decode_buffer/2, new_session/0]). session_defaults() -> {ok, true}. new_session() -> #http{}. %% @spec decode_buffer(Buffer::binary(),Session::record(http)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,Session) -> ts_http:decode_buffer(Buffer,Session). %% we should implement methods defined in rfc4918 get_message(Req=#http_request{method=Method},#state_rcv{session=S}) when Method == propfind; Method == proppatch; Method == copy; Method == move; Method == lock; Method == mkactivity; Method == unlock; Method == report; Method == 'version-control' -> M = string:to_upper(atom_to_list(Method)), {ts_http_common:http_body(M, Req),S}; get_message(Req=#http_request{method=Method},#state_rcv{session=S}) when Method == mkcol-> {ts_http_common:http_no_body("MKCOL", Req), S}; get_message(Req,State) -> ts_http:get_message(Req,State). parse_bidi(Data, State) -> ts_http:parse_bidi(Data,State). dump(A,B) -> ts_http:dump(A,B). parse(Data, State) -> ts_http_common:parse(Data, State). parse_config(Element, Conf) -> ts_config_http:parse_config(Element, Conf). add_dynparams(Subst, DynData, Param, HostData) -> ts_http:add_dynparams(Subst, DynData, Param, HostData). %%% method PROPFIND; entetes: Depth (optional); body: XML %%% method COPY; entete Destination: URL, If (optional), Overwrite (Optional), Depth; Body: XML (Optional) %%% method MOVE; entete Destination: URL, If (optional), Overwrite (Optional), Depth; Body: XML (Optional) %%% method PROPPATCH body: XML %%% method MKCOL %%% method LOCK; entete: Timeout (optional ?), If (Optional),Depth (Optional); Body: XML (optional ?) %%% method UNLOCK; entete: Lock-Token; Body: XML (optional ?) tsung-1.8.0/src/tsung/ts_utils.erl0000644000201100017670000011562414377756736016710 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. -module(ts_utils). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -include("ts_macros.hrl"). %% to get file_info record definition -include_lib("kernel/include/file.hrl"). %% user interface -export([debug/3, debug/4, get_val/1, init_seed/0, chop/1, elapsed/2, now_sec/0, node_to_hostname/1, add_time/2, keyumerge/3, key1search/2, level2int/1, mkey1search/2, datestr/0, datestr/1, size_or_length/1, erl_system_args/0, erl_system_args/1, setsubdir/1, export_text/1, foreach_parallel/2, spawn_par/3, inet_setopts/3, resolve/2, stop_all/2, stop_all/3, stop_all/4, join/2, split/2, split2/2, split2/3, make_dir/1, make_dir_raw/1, is_ip/1, from_https/1, to_https/1, keymax/2, check_sum/3, check_sum/5, clean_str/1, file_to_list/1, term_to_list/1, decode_base64/1, encode_base64/1, to_lower/1, randomstr/1,urandomstr/1,urandomstr_noflat/1, eval/1, list_to_number/1, time2sec/1, time2sec_hires/1, read_file_raw/1, init_seed/1, jsonpath/2, concat_atoms/1, ceiling/1, accept_loop/3, append_to_filename/3, splitchar/2, randombinstr/1,urandombinstr/1,log_transaction/1,conv_entities/1, wildcard/2, ensure_all_started/2, pmap/2, pmap/3, get_node_id/0, filtermap/2, new_ets/2, is_controller/0, spread_list/1, pack/1, random_alphanumstr/1]). level2int("debug") -> ?DEB; level2int("info") -> ?INFO; level2int("notice") -> ?NOTICE; level2int("warning") -> ?WARN; level2int("error") -> ?ERR; level2int("critical") -> ?CRIT; level2int("emergency") -> ?EMERG. -define(QUOT,"""). -define(APOS,"'"). -define(AMP,"&"). -define(GT,">"). -define(LT,"<"). -define(DUPSTR_SIZE,20). -define(DUPSTR,"qxvmvtglimieyhemzlxc"). -define(DUPBINSTR_SIZE,20). -define(DUPBINSTR,<<"qxvmvtglimieyhemzlxc">>). %%---------------------------------------------------------------------- %% Func: get_val/1 %% Purpose: return environment variable value for the current application %% Returns: Value | {undef_var, Var} %%---------------------------------------------------------------------- get_val(Var) -> case application:get_env(Var) of {ok, Val} -> ensure_string(Var, Val); undefined -> % undef, application not started, try to get var from stdlib case application:get_env(stdlib,Var) of undefined -> {undef_var, Var}; {ok,Val} -> ensure_string(Var, Val) end end. %% ensure atom to string conversion of environment variable %% This is intended to fix a problem making tsung run under Windows %% I convert parameter that are called from the command-line ensure_string(log_file, Atom) when is_atom(Atom) -> atom_to_list(Atom); ensure_string(proxy_log_file, Atom) when is_atom(Atom) -> atom_to_list(Atom); ensure_string(config_file, Atom) when is_atom(Atom) -> atom_to_list(Atom); ensure_string(exclude_tag, Atom) when is_atom(Atom) -> atom_to_list(Atom); ensure_string(_, Other) -> Other. %%---------------------------------------------------------------------- %% Func: debug/3 %% Purpose: print debug message if level is high enough %%---------------------------------------------------------------------- debug(From, Message, Level) -> debug(From, Message, [], Level). debug(From, Message, Args, Level) -> Debug_level = ?config(debug_level), if Level =< Debug_level -> error_logger:info_msg("~20s:(~p:~p) "++ Message, [From, Level, self()] ++ Args); true -> nodebug end. %%---------------------------------------------------------------------- %% Func: elapsed/2 %% Purpose: print elapsed time in milliseconds %% Returns: integer %%---------------------------------------------------------------------- elapsed({Before1, Before2, Before3}, {After1, After2, After3}) -> After = After1 * 1000000000 + After2 * 1000 + After3/1000, Before = Before1 * 1000000000 + Before2 * 1000 + Before3/1000, case After - Before of Neg when Neg < 0 -> % time duration must not be negative 0; Val -> Val end; elapsed(Before, After)-> Elapsed=After-Before, MicroSec = erlang:convert_time_unit(Elapsed, native, micro_seconds), MicroSec / 1000. %%---------------------------------------------------------------------- %% Func: chop/1 %% Purpose: remove trailing "\n" %%---------------------------------------------------------------------- chop(String) -> string:strip(String, right, 10). %%---------------------------------------------------------------------- %% Func: clean_str/1 %% Purpose: remove "\n" and space at the beginning and at that end of a string %%---------------------------------------------------------------------- clean_str(String) -> Str1 = string:strip(String, both, 10), Str2 = string:strip(Str1), Str3 = string:strip(Str2, both, 10), string:strip(Str3). %%---------------------------------------------------------------------- %% Func: init_seed/1 %%---------------------------------------------------------------------- init_seed(now)-> init_seed(); init_seed(A) when is_integer(A)-> %% in case of a distributed test, we don't want each launcher to %% have the same seed, therefore, we need to know the id of the %% node to set a reproductible but different seed for each launcher. Id=get_node_id(), ?DebugF("Seeding with ~p on node ~p~n",[Id,node()]), random:seed(1000*Id,-1000*A*Id,1000*A*A); init_seed({A,B}) when is_integer(A) and is_integer(B)-> Id=get_node_id(), ?DebugF("Seeding with ~p ~p ~p on node ~p~n",[A,B,Id,node()]), %% init_seed with 2 args is called by ts_client, with increasing %% values of A, and fixed B. If the seeds are too closed, the %% initial pseudo random values will be quite closed to each %% other. Trying to avoid this by using a multiplier big enough %% (because the algorithm use mod 30XXX , see random.erl). random:seed(4000*A*B*Id,-4000*B*A*Id,4000*Id*Id*A); init_seed({A,B,C}) -> random:seed(A,B,C). get_node_id() -> case string:tokens(atom_to_list(node()),"@") of ["tsung_control"++_,_] -> 123456; ["tsung"++Tail,_] -> {match, [I]} = re:run(Tail, "\\d+$", [{capture, all, list}]), %" add comment for erlang-mode bug list_to_integer(I); _ -> 654321 end. %% @spec is_controller() -> true|false %% @doc return true if the caller is running on the controller node %% @end is_controller() -> case string:tokens(atom_to_list(node()),"@") of ["tsung_control"++_,_] -> true; _ ->false end. %%---------------------------------------------------------------------- %% Func: init_seed/0 %%---------------------------------------------------------------------- init_seed()-> init_seed(?TIMESTAMP). %%---------------------------------------------------------------------- %% Func: now_sec/0 %% Purpose: returns unix like elapsed time in sec %% TODO: we should use erlang:system_time(seconds) when we drop < R18 compat %%---------------------------------------------------------------------- now_sec() -> time2sec(?TIMESTAMP). time2sec({MSec, Seconds, _}) -> Seconds+1000000*MSec. time2sec_hires({MSec, Seconds, MuSec}) -> Seconds+1000000*MSec+MuSec/1000000. %%---------------------------------------------------------------------- %% Func: add_time/2 %% Purpose: add given Seconds to given Time (same format as now()) %%---------------------------------------------------------------------- add_time({MSec, Seconds, MicroSec}, SecToAdd) when is_integer(SecToAdd)-> NewSec = Seconds +SecToAdd, case NewSec < 1000000 of true -> {MSec, NewSec, MicroSec}; false ->{MSec+ (NewSec div 1000000), NewSec-1000000, MicroSec} end; add_time(Time, SecToAdd) when is_integer(SecToAdd)-> MicroSec = erlang:convert_time_unit(Time, native, micro_seconds)+SecToAdd*1000000, erlang:convert_time_unit(MicroSec, micro_seconds, native). node_to_hostname(Node) -> [_Nodename, Hostname] = string:tokens( atom_to_list(Node), "@"), {ok, Hostname}. to_lower(String)-> string:to_lower(String). encode_base64(String)-> base64:encode_to_string(String). decode_base64(Base64)-> base64:decode_to_string(Base64). %%---------------------------------------------------------------------- %% Func: filtermap/2 %% Purpose lists:zf is called lists:filtermap in erlang R16B1 and newer %% %%---------------------------------------------------------------------- filtermap(Fun, List)-> case erlang:function_exported(lists, filtermap, 2) of true -> lists:filtermap(Fun,List); false -> lists:zf(Fun,List) end. %%---------------------------------------------------------------------- %% Func: key1search/2 %% Purpose: wrapper around httpd_utils module funs (maybe one day %% these functions will be added to the stdlib) %%---------------------------------------------------------------------- key1search(Tuple,String)-> proplists:get_value(String,Tuple). %%---------------------------------------------------------------------- %% Func: mkey1search/2 %% Purpose: multiple key1search: %% Take as input list of {Key, Value} tuples (length 2). %% Return the list of values corresponding to a given key %% It is assumed here that there might be several identical keys in the list %% unlike the lists:key... functions. %%---------------------------------------------------------------------- mkey1search(List, Key) -> Results = lists:foldl( fun({MatchKey, Value}, Acc) when MatchKey == Key -> [Value | Acc]; ({_OtherKey, _Value}, Acc) -> Acc end, [], List), case Results of [] -> undefined; Results -> lists:reverse(Results) end. %%---------------------------------------------------------------------- %% datestr/0 %% Purpose: print date as a string 'YYYYMMDD-HHMM' %%---------------------------------------------------------------------- datestr()-> datestr(erlang:localtime()). %%---------------------------------------------------------------------- %% datestr/1 %%---------------------------------------------------------------------- datestr({{Y,M,D},{H,Min,_S}})-> io_lib:format("~w~2.10.0b~2.10.0b-~2.10.0b~2.10.0b",[Y,M,D,H,Min]). %%---------------------------------------------------------------------- %% erl_system_args/0 %%---------------------------------------------------------------------- erl_system_args()-> erl_system_args(extended). erl_system_args(basic)-> Rsh = case init:get_argument(rsh) of {ok,[[Value]]} -> " -rsh " ++ Value; _ -> " " end, lists:append([Rsh, " -detached -setcookie ", atom_to_list(erlang:get_cookie()) ]); erl_system_args(extended)-> BasicArgs = erl_system_args(basic), SetArg = fun(A) -> case init:get_argument(A) of error -> " "; {ok,[[]]} -> " -" ++atom_to_list(A)++" "; {ok,[[Val|_]]} when is_list(Val)-> " -" ++atom_to_list(A)++" "++Val++" " end end, Shared = SetArg(shared), Hybrid = SetArg(hybrid), case {?config(smp_disable), erlang:system_info(otp_release)} of {true,"R"++_} -> Smp = " -smp disable "; {true,V} when (V =:= "17" orelse V =:= "18" orelse V =:= "19") -> Smp = " -smp disable "; {true,_} -> Smp = " +S 1 "; _ -> Smp = SetArg(smp) end, Inet = case init:get_argument(kernel) of {ok,[["inetrc",InetRcFile]]} -> ?LOGF("Get inetrc= ~p~n",[InetRcFile],?NOTICE), " -kernel inetrc '"++ InetRcFile ++ "'" ; _ -> " " end, Proto = case init:get_argument(proto_dist) of {ok,[["inet6_tcp"]]}-> ?LOG("IPv6 used for erlang distribution~n",?NOTICE), " -proto_dist inet6_tcp " ; _ -> " " end, ListenMin = case application:get_env(kernel,inet_dist_listen_min) of undefined -> ""; {ok, Min} -> " -kernel inet_dist_listen_min " ++ integer_to_list(Min)++ " " end, ListenMax = case application:get_env(kernel,inet_dist_listen_max) of undefined -> ""; {ok, Max} -> " -kernel inet_dist_listen_max " ++ integer_to_list(Max)++" " end, SSLCache = case application:get_env(ssl,session_cb) of {ok, CB} when is_atom(CB) -> " -ssl session_cb " ++ atom_to_list(CB)++" "; _ -> "" end, SSLLifetime = case application:get_env(ssl,session_lifetime) of {ok, Time} when is_integer(Time) -> " -ssl session_lifetime " ++ integer_to_list(Time)++" "; _ -> "" end, SSLCacheSize = case application:get_env(tsung,ssl_session_cache) of {ok, Reuse} when is_integer(Reuse)-> " -tsung reuse_sessions " ++ integer_to_list(Reuse)++" "; _ -> "" end, Threads= "+A "++integer_to_list(erlang:system_info(thread_pool_size))++" ", ProcessMax="+P "++integer_to_list(erlang:system_info(process_limit))++" ", Mea = case erlang:system_info(version) of "5.3" ++ _Tail -> " +Mea r10b "; _ -> " " end, lists:append([BasicArgs, Shared, Hybrid, Smp, Mea, Inet, Proto, Threads, ProcessMax,ListenMin,ListenMax,SSLCache,SSLLifetime,SSLCacheSize]). %%---------------------------------------------------------------------- %% setsubdir/1 %% Purpose: all log files are created in a directory whose name is the %% start date of the test. %% ---------------------------------------------------------------------- setsubdir(FileName) -> Date = datestr(), Path = filename:dirname(FileName), Base = filename:basename(FileName), Dir = filename:join(Path, Date), case file:make_dir(Dir) of ok -> {ok, {Dir, Base}}; {error, eexist} -> ?DebugF("Directory ~s already exist~n",[Dir]), {ok, {Dir, Base}}; Err -> ?LOGF("Can't create directory ~s (~p)!~n",[Dir, Err],?EMERG), {error, Err} end. %%---------------------------------------------------------------------- %% export_text/1 %% Purpose: Escape special characters `<', `&', `'' and `"' flattening %% the text. %%---------------------------------------------------------------------- export_text(T) -> export_text(T, []). export_text(Bin, Cont) when is_binary(Bin) -> export_text(binary_to_list(Bin), Cont); export_text([], Exported) -> lists:flatten(lists:reverse(Exported)); export_text([$< | T], Cont) -> export_text(T, [?LT | Cont]); export_text([$> | T], Cont) -> export_text(T, [?GT | Cont]); export_text([$& | T], Cont) -> export_text(T, [?AMP | Cont]); export_text([$' | T], Cont) -> %' export_text(T, [?APOS | Cont]); export_text([$" | T], Cont) -> %" export_text(T, [?QUOT | Cont]); export_text([C | T], Cont) -> export_text(T, [C | Cont]). %%---------------------------------------------------------------------- %% stop_all/2 %%---------------------------------------------------------------------- stop_all(Host, Name) -> stop_all(Host, Name, "Tsung"). stop_all([Host],Name,MsgName) -> VoidFun = fun(_A)-> ok end, stop_all([Host],Name,MsgName, VoidFun). stop_all([Host],Name,MsgName,Fun) when is_atom(Host) -> _List= net_adm:world_list([Host]), global:sync(), case global:whereis_name(Name) of undefined -> Msg = MsgName ++" is not running on " ++ atom_to_list(Host), erlang:display(Msg); Pid -> Controller_Node = node(Pid), Fun(Controller_Node), slave:stop(Controller_Node) end; stop_all(_,_,_,_)-> erlang:display("Bad Hostname"). %%---------------------------------------------------------------------- %% make_dir/1 %% Purpose: create directory. Missing parent directories ARE created %%---------------------------------------------------------------------- make_dir(DirName) -> make_dir_rec(DirName,file). make_dir_raw(DirName) -> make_dir_rec(DirName,prim_file). make_dir_rec(DirName,FileMod) when is_list(DirName) -> case FileMod:read_file_info(DirName) of {ok, #file_info{type=directory}} -> ok; {error,enoent} -> make_dir_rec("", FileMod,filename:split(DirName)); {error, Reason} -> {error,Reason} end. make_dir_rec(_Path, _FileMod, []) -> ok; make_dir_rec(Path, FileMod,[Parent|Childs]) -> CurrentDir=filename:join([Path,Parent]), case FileMod:read_file_info(CurrentDir) of {ok, #file_info{type=directory}} -> make_dir_rec(CurrentDir, FileMod,Childs); {error,enoent} -> case FileMod:make_dir(CurrentDir) of ok -> make_dir_rec(CurrentDir, FileMod, Childs); {error, eexist} -> make_dir_rec(CurrentDir, FileMod, Childs); Error -> Error end; {error, Reason} -> {error,Reason} end. %% check if a string is an IPv4 address (as "192.168.0.1") is_ip(String) when is_list(String) -> EightBit="(2[0-4][0-9]|25[0-5]|1[0-9][0-9]|[0-9][0-9]|[0-9])", RegExp = lists:append(["^",EightBit,"\.",EightBit,"\.",EightBit,"\.",EightBit,"$"]), %" case re:run(String, RegExp) of {match,_} -> true; _ -> false end; is_ip(_) -> false. %%---------------------------------------------------------------------- %% to_https/1 %% Purpose: rewrite https URL, to act as a pure non ssl proxy %%---------------------------------------------------------------------- to_https({url, "http://-"++Rest})-> "https://" ++ Rest; to_https({url, URL})-> URL; to_https({request, {body,Data}}) when is_list(Data) -> %% body request, no headers {ok, re:replace(Data,"http://-","https://",[global])}; to_https({request, S="CONNECT"++_Rest}) -> {ok,S}; to_https({request, []}) -> {ok, []}; to_https({request, String}) when is_list(String) -> EndOfHeader = string:str(String, "\r\n\r\n"), Header = string:substr(String, 1, EndOfHeader - 1) ++ "\r\n", Body = string:substr(String, EndOfHeader + 4), ReOpts=[global,{return,list}], TmpHeader = re:replace(Header,"http://-","https://",ReOpts), TmpHeader2 = re:replace(TmpHeader,"Accept-Encoding: [0-9,a-z_ ]+\r\n","",ReOpts++[caseless]), RealHeader = re:replace(TmpHeader2,"Host: -","Host: ",ReOpts++[caseless]), RealBody = re:replace(Body,"http://-","https://",ReOpts), RealString = RealHeader++ "\r\n" ++ RealBody, {ok, RealString}. %% @spec from_https(string()) -> {ok, string() | iodata()} %% @doc replace https links with 'http://-' %% @end from_https(String) when is_list(String)-> ReOpts=[{newline,crlf},multiline,global,caseless], %% remove Secure from Set-Cookie (TSUN-120) TmpData = re:replace(String,"(.*set-cookie:.*); *secure(.*$.*$)","\\1\\2",ReOpts), Data=re:replace(TmpData,"https://","http://-",[global]), {ok, Data}. %% concatenate a list of atoms concat_atoms(Atoms) when is_list(Atoms) -> String =lists:foldl(fun(A,Acc) -> Acc++atom_to_list(A) end, "", Atoms), list_to_atom(String). %% A Perl-style join --- concatenates all strings in Strings, %% separated by Sep. join(_Sep, []) -> []; join(Sep, List) when is_list(List)-> ToStr = fun(A) when is_integer(A) -> integer_to_list(A); (A) when is_list(A) -> A; (A) when is_float(A) -> io_lib:format("~.3f",[A]); (A) when is_atom(A) -> atom_to_list(A); (A) when is_binary(A) -> binary_to_list(A) end, string:join(lists:map(ToStr,List), Sep). %% split a string given a string (at first occurrence of char) split(String,Chr) when is_list(String), is_list(Chr) -> re:split(String,Chr,[{return,list}]); split(String,Chr) when is_binary(String), is_binary(Chr) -> binary:split(String,[Chr],[global]). %% split a string given a char (faster) splitchar(String,Chr) -> splitchar2(String,Chr,[],[]). splitchar2([],_,[],Acc) -> lists:reverse(Acc); splitchar2([],_,AccChr,Acc) -> lists:reverse([lists:reverse(AccChr)|Acc]); splitchar2([Chr|String],Chr,AccChr,Acc) -> splitchar2(String,Chr,[],[lists:reverse(AccChr)|Acc]); splitchar2([Other|String],Chr,AccChr,Acc) -> splitchar2(String,Chr,[Other|AccChr],Acc). %% split a string in 2 (at first occurrence of char) split2(String,Chr) -> split2(String,Chr,nostrip). split2(String,Chr,strip) -> % split and strip blanks {A, B} = split2(String,Chr,nostrip), {string:strip(A), string:strip(B)}; split2(String,Chr,nostrip) -> case string:chr(String, Chr) of 0 -> {String,[]}; Pos -> {string:substr(String,1,Pos-1), string:substr(String,Pos+1)} end. foreach_parallel(Fun, List)-> SpawnFun = fun(A) -> spawn(?MODULE, spawn_par, lists:append([[Fun,self()], [A]])) end, lists:foreach(SpawnFun, List), wait_pids(length(List)). wait_pids(0) -> done; wait_pids(N) -> receive {ok, _Pid, _Res } -> wait_pids(N-1) after ?TIMEOUT_PARALLEL_SPAWN -> {error, {timeout, N}} % N missing answer end. spawn_par(Fun, PidFrom, Args) -> Res = Fun(Args), PidFrom ! {ok, self(), Res}. %%---------------------------------------------------------------------- %% Func: inet_setopts/3 %% Purpose: set inet options depending on the protocol (gen_tcp, gen_udp, %% ssl) %%---------------------------------------------------------------------- inet_setopts(_, none, _) -> %socket was closed before none; inet_setopts(ssl6, Socket, Opts) -> inet_setopts(ssl, Socket, Opts); inet_setopts(ssl, Socket, Opts) -> case ssl:setopts(Socket, Opts) of ok -> Socket; {error, closed} -> none; Error -> ?LOGF("Error while setting ssl options ~p ~p ~n", [Opts, Error], ?ERR), none end; inet_setopts(gen_tcp6, Socket, Opts)-> inet_setopts(gen_tcp, Socket, Opts); inet_setopts(gen_udp6, Socket, Opts)-> inet_setopts(gen_udp, Socket, Opts); inet_setopts(_Type, Socket, Opts)-> case inet:setopts(Socket, Opts) of ok -> Socket; {error, closed} -> none; Error -> ?LOGF("Error while setting inet options ~p ~p ~n", [Opts, Error], ?ERR), none end. %%---------------------------------------------------------------------- %% Func: check_sum/3 %% Purpose: check sum of int equals 100. %% Args: List of tuples, index of int in tuple, Error msg %% Returns ok | {error, {bad_sum, Msg}} %%---------------------------------------------------------------------- check_sum(RecList, Index, ErrorMsg) -> %% popularity may be a float number. 5.10-2 precision check_sum(RecList, Index, 100, 0.05, ErrorMsg). check_sum(RecList, Index, Total, Epsilon, ErrorMsg) -> %% we use the tuple representation of a record ! Sum = lists:foldl(fun(X, Sum) -> element(Index,X)+Sum end, 0, RecList), Delta = abs(Sum - Total), case Delta < Epsilon of true -> ok; false -> {error, {bad_sum, Sum ,ErrorMsg}} end. %%---------------------------------------------------------------------- %% Func: file_to_list/1 %% Purpose: read a file line by line and put them in a list %% Args: filename %% Returns {ok, List} | {error, Reason} %%---------------------------------------------------------------------- file_to_list(FileName) -> case file:open(FileName, [read]) of {error, Reason} -> {error, Reason}; {ok , File} -> Lines = read_lines(File), file:close(File), {ok, Lines} end. read_lines(FD) ->read_lines(FD,io:get_line(FD,""),[]). read_lines(_FD, eof, L) -> lists:reverse(L); read_lines(FD, Line, L) -> read_lines(FD, io:get_line(FD,""),[chop(Line)|L]). %%---------------------------------------------------------------------- %% Func: keyumerge/3 %% Purpose: Same as lists:keymerge, but remove duplicates (use items from A) %% Returns: List %%---------------------------------------------------------------------- keyumerge(_N,[],B)->B; keyumerge(N,[A|Rest],B)-> Key = element(N,A), % remove old values if it exists NewB = lists:keydelete(Key, N, B), keyumerge(N,Rest, [A|NewB]). %%---------------------------------------------------------------------- %% Func: keymax/2 %% Purpose: Return Max of Nth element of a list of tuples %% Returns: Number %%---------------------------------------------------------------------- keymax(_N,[])-> 0; keymax(N,[L])-> element(N,L); keymax(N,[E|Tail])-> keymax(N,Tail,element(N,E)). keymax(_N,[],Max)-> Max; keymax(N,[E|Tail],Max)-> keymax(N,Tail,lists:max([Max,element(N,E)])). %%-------------------------------------------------------------------- %% Function: resolve/2 %% Description: return cached hostname or gethostbyaddr for given ip %%-------------------------------------------------------------------- resolve(Ip, Cache) -> case lists:keysearch(Ip, 1, Cache) of {value, {Ip, ReverseHostname}} -> {ReverseHostname, Cache}; false -> case inet:gethostbyaddr(Ip) of {ok, {hostent,ReverseHostname,_,inet,_,_}} -> %% cache dns result and return it ?LOGF("Add ~p -> ~p to DNS cache ~n", [Ip, ReverseHostname],?DEB), {ReverseHostname, [{Ip, ReverseHostname} | Cache]}; {error, Reason} -> ?LOGF("DNS resolution error on ~p: ~p~n", [Ip, Reason],?WARN), %% cache dns name as IP : {ip, ip} and return Ip NewCache = lists:keymerge(1, Cache, [{Ip, Ip}]), {Ip, NewCache} end end. %%---------------------------------------------------------------------- %% @spec urandomstr_noflat(Size::integer()) ->string() %% @doc generate pseudo-random list of given size. Implemented by %% duplicating list of fixed size to be faster. unflatten version %% @end %%---------------------------------------------------------------------- urandomstr_noflat(Size) when is_integer(Size) , Size >= ?DUPSTR_SIZE -> Msg= lists:duplicate(Size div ?DUPSTR_SIZE,?DUPSTR), case Size rem ?DUPSTR_SIZE of 0-> Msg; Rest -> lists:append(Msg,urandomstr_noflat(Rest)) end; urandomstr_noflat(Size) when is_integer(Size), Size >= 0 -> lists:nthtail(?DUPSTR_SIZE-Size, ?DUPSTR). %%---------------------------------------------------------------------- %% @spec urandombinstr(Size::integer()) ->binary() %% @doc same as urandomstr/1, but returns a binary. %% @end %%---------------------------------------------------------------------- urandombinstr(Size) when is_integer(Size) , Size >= ?DUPBINSTR_SIZE -> Loop = Size div ?DUPBINSTR_SIZE, Rest = Size rem ?DUPBINSTR_SIZE, Res=lists:foldl(fun(_X,Acc)-> <> end, << >>,lists:seq(1,Loop)), << Res/binary, ?DUPBINSTR:Rest/binary>>; urandombinstr(Size) when is_integer(Size), Size >= 0 -> <> . %%---------------------------------------------------------------------- %% @spec urandomstr(Size::integer()) ->string() %% @doc same as urandomstr_noflat/1, but returns a flat list. %% @end %%---------------------------------------------------------------------- urandomstr(Size) when is_integer(Size), Size >= 0 -> lists:flatten(urandomstr_noflat(Size)). %%---------------------------------------------------------------------- %% @spec randomstr(Size::integer()) ->string() %% @doc returns a random string. slow if Size is high. %% @end %%---------------------------------------------------------------------- randomstr(Size) when is_integer(Size), Size >= 0 -> lists:map(fun (_) -> random:uniform(25) + $a end, lists:seq(1,Size)). random_alphanumstr(Size) when is_integer(Size), Size >= 0 -> AllowedChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", S = length(AllowedChars), lists:map(fun (_) -> lists:nth(random:uniform(S), AllowedChars) end, lists:seq(1,Size)). %%---------------------------------------------------------------------- %% @spec randombinstr(Size::integer()) ->binary() %% @doc returns a random binary string. slow if Size is high. %% @end %%---------------------------------------------------------------------- randombinstr(0) -> <<>>; randombinstr(Size) when is_integer(Size), Size > 0 -> randombinstr(Size,<<>>). randombinstr(0,Bin) -> Bin; randombinstr(Size,Bin) -> C=random:uniform(25)+$a, randombinstr(Size-1, << Bin/binary, C >>). %%---------------------------------------------------------------------- %% @spec eval(string()) -> term() %% @doc evaluate strings as Erlang code at runtime %% @end %%---------------------------------------------------------------------- eval(Code) -> {ok, Scanned, _} = erl_scan:string(lists:flatten(Code)), {ok, Parsed} = erl_parse:parse_exprs(Scanned), {value, Result, _} = erl_eval:exprs(Parsed, erl_eval:new_bindings()), Result. %%---------------------------------------------------------------------- %% @spec list_to_number(string()) -> integer() | float() %% @doc convert a 'number' to either int or float %% @end %%---------------------------------------------------------------------- list_to_number(Number) -> try list_to_integer(Number) of Int -> Int catch error:_Reason -> list_to_float(Number) end. term_to_list(I) when is_integer(I)-> integer_to_list(I); term_to_list(I) when is_atom(I)-> atom_to_list(I); term_to_list(I) when is_list(I)-> I; term_to_list(I) when is_float(I)-> float_to_list(I); term_to_list(B) when is_binary(B)-> binary_to_list(B). read_file_raw(File) when is_list(File) -> case {file:open(File,[read,raw,binary]), file:read_file_info(File)} of { {ok,IODev}, {ok,#file_info{size=Size} } } -> case file:pread(IODev,0,Size) of {ok, Res} -> file:close(IODev), {ok, Res, Size}; Else -> ?LOGF("pread file ~p of size ~p: ~p~n",[File,Size,Else],?NOTICE), file:close(IODev), Else end; {{ok,IODev}, {error, Reason} } -> file:close(IODev), {error,Reason}; {{error,Reason},_} -> {error, Reason} end. %%---------------------------------------------------------------------- %% @spec jsonpath(JSONPath::string(),JSON::iolist()) -> term() %% @doc very limited implementation of JSONPath from JSON struct. %% @end %%---------------------------------------------------------------------- jsonpath("$."++JSONPath,JSON) -> jsonpath(JSONPath,JSON); jsonpath(JSONPath,JSON) -> Fun= fun(A) -> case catch list_to_integer(A) of I when is_integer(I) -> I+1; _Error -> list_to_binary(A) end end, Str=re:replace(JSONPath,"\\[(.*?)\\]","\.\\1",[{return,list},global]), Keys=lists:map(Fun, string:tokens(Str,".")), json_get_bin(Keys,JSON). json_get_bin([],Val) -> Val; json_get_bin([_Key|_Keys],undefined) -> undefined; json_get_bin([N|Keys],L) when is_integer(N), N =< length(L) -> Val = lists:nth(N,L), json_get_bin(Keys,Val); json_get_bin([N|Keys], L) when N =:= <<"*">>, is_list(L) -> lists:map(fun(A) -> json_get_bin(Keys,A) end, L); json_get_bin([N|Keys],Val) when N =:= <<"*">> -> json_get_bin(Keys,Val); json_get_bin([<<"?",Expr/binary>> | Keys],L) when is_list(L) -> case string:tokens(binary_to_list(Expr),"=") of [Key,Val] -> Fun = fun(S) -> case json_get_bin([list_to_binary(Key)],S) of Int when is_integer(Int) -> integer_to_list(Int) =:= Val; Other when is_binary(Other)-> binary_to_list(Other) =:= Val end end, case lists:filter(Fun,L) of [] -> undefined; [Res] -> json_get_bin(Keys,Res); Res -> lists:map(fun(A) -> json_get_bin(Keys,A) end, Res) end; _ -> undefined end; json_get_bin([Key|Keys],{struct,JSON}) when is_list(JSON) -> Val = proplists:get_value(Key,JSON), json_get_bin(Keys,Val); json_get_bin(_,_) -> undefined. %% Map function F over list L in parallel. pmap(F, L) -> Parent = self(), [receive {Pid, Result} -> Result end || Pid <- [spawn(fun() -> Parent ! {self(), F(X)} end) || X <- L]]. %% Map function F over list L in parallel, with maximum NProcs in parallel %% FIXME: handle timeout pmap(F, L, NProcs) -> pmap(F, L, NProcs,""). pmap(F, L, NProcs, Res) when length(L) > NProcs-> {Head, Tail} = lists:split(NProcs,L), Parent = self(), lists:foldl(fun(X, Acc) -> spawn(fun() -> Parent ! {pmap, self(), F(X), Acc} end), Acc+1 end, 0, Head), NewRes = wait_result(NProcs,[]), pmap(F,Tail, NProcs, Res ++ NewRes); pmap(F, L, _NProcs, Acc) -> Acc ++ pmap(F,L). wait_result(0, Res)-> {_Ids, RealRes} = lists:unzip(lists:keysort(1, Res)), RealRes; wait_result(Nprocs, Res)-> receive {pmap, _Pid, Result, Id} -> NewRes = Res ++ [{Id, Result}], wait_result(Nprocs-1, NewRes) end. %% ceiling(X) -> T = erlang:trunc(X), case (X - T) of Neg when Neg < 0 -> T; Pos when Pos > 0 -> T + 1; _ -> T end. %%-------------------------------------------------------------------- %% Func: accept_loop/3 %% Purpose: infinite listen/accept loop, delegating handling of accepts %% to the gen_server proper. %% Returns: only returns by throwing an exception %%-------------------------------------------------------------------- accept_loop(PPid, Tag, ServerSock)-> case case gen_tcp:accept(ServerSock) of {ok, ClientSock} -> ok = gen_tcp:controlling_process(ClientSock, PPid), gen_server:call(PPid, {accepted, Tag, ClientSock}); Error -> gen_server:call(PPid, {accept_error, Tag, Error}) end of continue -> accept_loop(PPid, Tag, ServerSock); _-> normal end. append_to_filename(Filename, From, To) -> case re:replace(Filename,From,To, [{return,list},global] ) of Filename -> Filename ++"." ++ To; RealName -> RealName end. log_transaction([]) -> "-"; log_transaction([{TransactionName,_}| _Tail]) -> TransactionName. %%-------------------------------------------------------------------- %% Func: conv_entities/1 %% Purpose: Convert html entities to string %%-------------------------------------------------------------------- conv_entities(Binary)-> conv_entities(Binary,[]). conv_entities(<< >>,Acc) -> list_to_binary(Acc); conv_entities(<< "&", T/binary >> ,Acc) -> conv_entities(T,[ Acc, << "&">>]); conv_entities(<< "<", T/binary >>,Acc) -> conv_entities(T,[ Acc, << "<">>]); conv_entities(<< ">", T/binary >>,Acc) -> conv_entities(T,[ Acc, << ">">>]); conv_entities(<<""", T/binary >>,Acc) -> conv_entities(T,[ Acc, << "\"">>]); conv_entities(<<"'", T/binary >>,Acc) -> conv_entities(T,[ Acc, << "'">>]); conv_entities(<>,Acc) -> conv_entities(T,[ Acc, H]). %% start an application and it's dependencies recursively %% does the same as application:ensure_all_started (only in R16B2) ensure_all_started(App, Type) -> start_ok(App, Type, application:start(App, Type)). start_ok(_App, _Type, ok) -> ok; start_ok(_App, _Type, {error, {already_started, _App}}) -> ok; start_ok(App, Type, {error, {not_started, Dep}}) -> ok = ensure_all_started(Dep, Type), ensure_all_started(App, Type); start_ok(App, _Type, {error, Reason}) -> erlang:error({app_start_failed, App, Reason}). wildcard(Wildcard,Names) -> PatternTmp = re:replace("^"++Wildcard,"\\*",".*",[{return,list}]), Pattern = re:replace(PatternTmp,"\\?",".{1}",[{return,list}]) ++ "$" , lists:filter(fun(N) -> re:run(N, Pattern) =/= nomatch end, Names). %% dummy comment with a " "to circumvent an erlang-mode bug in emacs" %%-------------------------------------------------------------------- %% Func: new_ets/1 %% Purpose: Wrapper for ets:new/1 used in external modules %% @spec new_ets(Prefix::binary(), UserId::integer()) -> string() %% @doc init an ets:table %% @end %%-------------------------------------------------------------------- new_ets(Prefix, UserId)-> EtsName = binary_to_list(Prefix) ++ "_" ++ integer_to_list(UserId), ?LOGF("create ets:table ~p ~n", [EtsName], ?INFO), ets:new(list_to_atom(EtsName), []). size_or_length(Data) when is_binary(Data) -> size(Data); size_or_length(Data) when is_list(Data) -> length(Data). %% given a list with successives duplicates, try to spread duplicates %% all over the list. e.g. [a,a,a,b,b,c,c] -> [a,b,c,a,b,c,a] spread_list(List) -> spread_list2(pack(List),[]). spread_list2([], Res) -> Res; spread_list2(PackedList, OldRes) -> Fun = fun([A], {Res, ResTail}) -> {[A|Res], ResTail}; ([A|ATail], {Res, ResTail}) -> {[A|Res], [ATail|ResTail]} end, {Res, Tail} = lists:foldl(Fun, {[],[]}, PackedList), spread_list2(lists:reverse(Tail), OldRes ++ lists:reverse(Res)). %% pack duplicates into sublists %% http://lambdafoo.com/blog/2008/02/26/99-erlang-problems-1-15/ pack([]) -> []; pack([H|[]]) -> [[H]]; pack([H,H|C]) -> [Head|Tail] = pack([H|C]), X = lists:append([H],Head), [X|Tail]; pack([H,H2|C]) -> if H =/= H2 -> [[H]|pack([H2|C])] end. tsung-1.8.0/src/tsung/tsung.erl0000644000201100017670000000457014377756736016177 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. -module(tsung). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([start/0, start/2, stop/1]). -behaviour(application). -include("ts_macros.hrl"). %% start the application with it's dependencies start() -> ts_utils:ensure_all_started(tsung, permanent). %%---------------------------------------------------------------------- %% Func: start/2 %% Returns: {ok, Pid} | %% {ok, Pid, State} | %% {error, Reason} %%---------------------------------------------------------------------- start(_Type, _StartArgs) -> % error_logger:tty(false), ?LOG("open logfile ~n",?DEB), LogFileEnc = ts_config_server:decode_filename(?config(log_file)), LogFile = filename:join(LogFileEnc, atom_to_list(node()) ++ ".log"), LogDir = filename:dirname(LogFile), ok = ts_utils:make_dir(LogDir), error_logger:logfile({open, LogFile}), ?LOG("ok~n",?DEB), case ts_sup:start_link() of {ok, Pid} -> {ok, Pid}; Error -> ?LOGF("Can't start supervisor ! ~p ~n",[Error],?ERR), Error end. %%---------------------------------------------------------------------- %% Func: stop/1 %% Returns: any %%---------------------------------------------------------------------- stop(_State) -> stop. tsung-1.8.0/src/tsung/ts_udp.erl0000644000201100017670000000436114377756736016333 0ustar nniclausdream%%% %%% Copyright 2012 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 7 sep 2012 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_udp). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). protocol_options(#proto_opts{udp_rcv_size=Rcv, udp_snd_size=Snd}) -> [binary, {active, once}, {recbuf, Rcv}, {sndbuf, Snd} ]. %% -> {ok, Socket} connect(_Host, _Port, Opts, _Timeout) -> gen_udp:open(0, Opts). %% send/3 -> ok | {error, Reason} send(Socket, Data, [{host, Host}, {port, Port}]) -> gen_udp:send(Socket, Host,Port, Data). close(none) -> ok; close(Socket) -> gen_udp:close(Socket). % set_opts/2 -> socket() set_opts(none, _Opts) -> none; set_opts(Socket, Opts) -> inet:setopts(Socket, Opts), Socket. normalize_incomming_data(Socket, {udp, Socket,_IP,_InPortNo, Data}) -> ?DebugF("UDP packet received: size=~p ~n",[size(Data)]), {gen_ts_transport, Socket, Data}; normalize_incomming_data(_Socket, X) -> X. %%Other, non gen_udp packet. tsung-1.8.0/src/tsung/ts_udp6.erl0000644000201100017670000000372614377756736016425 0ustar nniclausdream%%% %%% Copyright 2012 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 7 sep 2012 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_udp6). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). protocol_options(Opts) -> [inet6] ++ ts_udp:protocol_options(Opts). %% -> {ok, Socket} connect(_Host, _Port, Opts, _Timeout) -> gen_udp:open(0, Opts). %% send/3 -> ok | {error, Reason} send(Socket, Data, Opts) -> ts_udp:send(Socket, Data, Opts). close(Socket) -> ts_udp:close(Socket). % set_opts/2 -> socket() set_opts(none, _Opts) -> none; set_opts(Socket, Opts) -> inet:setopts(Socket, Opts), Socket. normalize_incomming_data(Socket, Data) -> ts_udp:normalize_incomming_data(Socket,Data). tsung-1.8.0/src/tsung/ts_tcp.erl0000644000201100017670000000617314377756736016334 0ustar nniclausdream%%% %%% Copyright 2010 © ProcessOne %%% %%% Author : Eric Cestari %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_tcp). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). protocol_options(Proto=#proto_opts{tcp_reuseport = true}) -> Opts= [{raw, 1, 15, <<1:32/native>>}] ++ protocol_options(Proto#proto_opts{tcp_reuseport = false}), ?DebugF("TCP Real opts: ~p ~n", [Opts]), Opts; protocol_options(Proto=#proto_opts{ip_transparent = true}) -> Opts= [{raw, 0, 19, <<1:32/native>>} ] ++ protocol_options(Proto#proto_opts{ip_transparent = false}), ?DebugF("TCP Real opts: ~p ~n", [Opts]), Opts; protocol_options(Proto=#proto_opts{ip_bind_address_no_port = true}) -> Opts= [{raw, 0, 24, <<1:32/native>>} ] ++ protocol_options(Proto#proto_opts{ip_bind_address_no_port = false}), ?DebugF("TCP Real opts: ~p ~n", [Opts]), Opts; protocol_options(#proto_opts{tcp_rcv_size = Rcv, tcp_snd_size = Snd, tcp_reuseaddr = Reuseaddr}) -> [binary, {active, once}, {reuseaddr, Reuseaddr}, {recbuf, Rcv}, {sndbuf, Snd}, {keepalive, true} %% FIXME: should be an option ]. %% -> {ok, Socket} connect(Host, Port, Opts, ConnectTimeout) -> gen_tcp:connect(Host, Port, opts_to_tcp_opts(Opts), ConnectTimeout). opts_to_tcp_opts(Opts) -> Opts. %% send/3 -> ok | {error, Reason} send(Socket, Data, _Opts) -> gen_tcp:send(Socket, Data). close(none) -> ok; close(Socket) -> gen_tcp:close(Socket). % set_opts/2 -> socket() set_opts(none, _Opts) -> none; set_opts(Socket, Opts) -> inet:setopts(Socket, Opts), Socket. normalize_incomming_data(Socket, {tcp, Socket, Data}) -> {gen_ts_transport, Socket, Data}; normalize_incomming_data(Socket, {tcp_closed, Socket}) -> {gen_ts_transport, Socket, closed}; normalize_incomming_data(Socket, {tcp_error, Socket, Error}) -> {gen_ts_transport, Socket, error, Error}; normalize_incomming_data(_Socket, X) -> X. %%Other, non gen_tcp packet. tsung-1.8.0/src/tsung/ts_tcp6.erl0000644000201100017670000000373414377756736016422 0ustar nniclausdream%%% %%% Copyright 2012 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 7 sep 2012 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_tcp6). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). protocol_options(Opts) -> [inet6]++ts_tcp:protocol_options(Opts). connect(Host, Port, Opts, ConnectTimeout) -> gen_tcp:connect(Host, Port, Opts, ConnectTimeout). %% send/3 -> ok | {error, Reason} send(Socket, Data, _Opts) -> gen_tcp:send(Socket, Data). close(Socket) -> ts_tcp:close(Socket). % set_opts/2 -> socket() set_opts(none, _Opts) -> none; set_opts(Socket, Opts) -> inet:setopts(Socket, Opts), Socket. normalize_incomming_data(Socket,Data) -> ts_tcp:normalize_incomming_data(Socket,Data). tsung-1.8.0/src/tsung/ts_sup.erl0000644000201100017670000000755714377756736016364 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. -module(ts_sup). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -include("ts_macros.hrl"). -behaviour(supervisor). %% External exports -export([start_link/0, start_cport/1, has_cport/1]). %% supervisor callbacks -export([init/1]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> ?LOG("starting supervisor ...~n",?INFO), supervisor:start_link({local, ?MODULE}, ?MODULE, []). start_cport({Node, CPortName}) -> ?LOGF("starting cport server ~p on node ~p ~n",[CPortName, Node],?INFO), PortServer = {CPortName, {ts_cport, start_link, [CPortName]}, transient, 2000, worker, [ts_cport]}, supervisor:start_child({?MODULE, Node}, PortServer). has_cport(Node) -> Children = supervisor:which_children({?MODULE, Node}), lists:any(fun({_,_,_,[ts_cport]}) -> true; (_) -> false end, Children). %%%---------------------------------------------------------------------- %%% Callback functions from supervisor %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, {SupFlags, [ChildSpec]}} | %% ignore | %% {error, Reason} %%---------------------------------------------------------------------- init([]) -> ?LOG("starting",?INFO), ClientsSup = {ts_client_sup, {ts_client_sup, start_link, []}, permanent, 2000, supervisor, [ts_client_sup]}, Launcher = {ts_launcher, {ts_launcher, start, []}, transient, 2000, worker, [ts_launcher]}, StaticLauncher = {ts_launcher_static, {ts_launcher_static, start, []}, transient, 2000, worker, [ts_launcher_static]}, LauncherManager = {ts_launcher_mgr, {ts_launcher_mgr, start, []}, transient, 2000, worker, [ts_launcher_mgr]}, SessionCache = {ts_session_cache, {ts_session_cache, start, []}, transient, 2000, worker, [ts_session_cache]}, MonCache = {ts_mon_cache, {ts_mon_cache, start, []}, transient, 2000, worker, [ts_mon_cache]}, LocalMon = {ts_local_mon, {ts_local_mon, start, []}, transient, 2000, worker, [ts_local_mon]}, IPScan = {ts_ip_scan, {ts_ip_scan, start_link, []}, transient, 2000, worker, [ts_ip_scan]}, {ok,{{one_for_one,?retries,10}, [IPScan, LauncherManager, SessionCache, MonCache, LocalMon, ClientsSup, StaticLauncher,Launcher ]}}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- tsung-1.8.0/src/tsung/ts_stats.erl0000644000201100017670000001300714377756736016676 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% Random Generators for several probability distributions -module(ts_stats). -created('Date: 2000/10/20 13:58:56 nniclausse Exp '). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([exponential/1, exponential/2, pareto/2, normal/0, normal/1, normal/2, uniform/2, invgaussian/2, mean/1, mean/3, variance/1, meanvar/4, meanvar_minmax/6, stdvar/1]). -import(math, [log/1, pi/0, sqrt/1, pow/2]). -record(pareto, {a = 1 , beta}). -record(normal, {mean = 0 , stddev= 1 }). -record(invgaussian, {mu , lambda}). %% get n samples from a function F with parameter Param sample (F, Param, N) -> sample(F, [], Param, N-1). sample (F, X, Param, 0) -> [F(Param) | X] ; sample (F, X, Param, N) -> sample(F, [F(Param)|X], Param, N-1 ). uniform(Min,Max)-> Min+random:uniform(Max-Min+1)-1. %% random sample from an exponential distribution exponential(Param) -> -math:log(random:uniform())/Param. %% N samples from an exponential distribution exponential(Param, N) -> sample(fun(X) -> exponential(X) end , Param, N). %% random sample from a Pareto distribution pareto(#pareto{a=A, beta=Beta}) -> A/(math:pow(random:uniform(), 1/Beta)). %% if a list is given, construct a record for the parameters pareto([A, Beta], N) -> pareto(#pareto{a = A , beta = Beta }, N); %% N samples from a Pareto distribution pareto(Param, N) -> sample(fun(X) -> pareto(X) end , Param, N). invgaussian([Mu,Lambda],N) -> invgaussian(#invgaussian{mu=Mu,lambda=Lambda},N); invgaussian(Param,N) -> sample(fun(X) -> invgaussian(X) end , Param, N). %% random sample from a Inverse Gaussian distribution invgaussian(#invgaussian{mu=Mu, lambda=Lambda}) -> Y = Mu*pow(normal(), 2), X1 = Mu+Mu*Y/(2*Lambda)-Mu*sqrt(4*Lambda*Y+pow(Y,2))/(2*Lambda), U = random:uniform(), X = (Mu/(Mu+X1))-U, case X >=0 of true -> X1; false -> Mu*Mu/X1 end. normal() -> [Val] = normal(#normal{},1), Val. normal([Mean,StdDev],N) -> normal(#normal{mean=Mean,stddev=StdDev},N); normal(Param,N) -> sample(fun(X) -> normal(X) end , Param, N). normal(N) when is_integer(N)-> normal(#normal{},N); normal(#normal{mean=M,stddev=S}) -> normal_boxm(M,S,0,0,1). %%% use the polar form of the Box-Muller transformation normal_boxm(M,S,X1,_X2,W) when W < 1-> W2 = sqrt( (-2.0 * log( W ) ) / W ), Y1 = X1 * W2, M + Y1 * S; normal_boxm(M,S,_,_,_W) -> X1 = 2.0 * random:uniform() - 1.0, X2 = 2.0 * random:uniform() - 1.0, normal_boxm(M,S,X1,X2,X1 * X1 + X2 * X2). %%% %% incremental computation of the mean mean(Esp, [], _) -> Esp; mean(Esp, [X|H], I) -> Next = I+1, mean((Esp+(X-Esp)/(Next)), H, Next). %% compute the mean of a list mean([]) -> 0; mean(H) -> mean(0, H, 0). %% @spec meanvar(Esp::number(),Var::number(),X::list() | number(),I::integer()) -> %% {NewEsp::number(), NewVar::number(), Next::integer()} %% @doc incremental computation of the mean and variance together. The %% algorithm should limit the round-off errors %% @end %% single value meanvar(Esp, Var, X, I) when is_number(X) -> Next = I+1, C = X - Esp, NewEsp = (X+Esp*I)/(Next), NewVar = Var+C*(X-NewEsp), { NewEsp, NewVar, Next }; %% list of samples meanvar(Esp, Var,[], I) -> {Esp, Var, I}; meanvar(Esp, Var, [X|H], I) -> {NewEsp, NewVar, Next} = meanvar(Esp,Var,X,I), meanvar(NewEsp, NewVar, H, Next). %% compute min and max also meanvar_minmax(Esp, Var, Min, Max, X, I) when is_number(X)-> meanvar_minmax(Esp, Var, Min, Max, [X], I); meanvar_minmax(Esp, Var, Min, Max, [], I) -> {Esp, Var, Min, Max, I}; meanvar_minmax(Esp, Var, 0, 0, [X|H], I) -> % first data, set min and max meanvar_minmax(Esp, Var, X, X, [X|H], I); meanvar_minmax(Esp, Var, Min, Max, [X|H], I) -> {NewEsp, NewVar, Next} = meanvar(Esp,Var,X,I), if X > Max -> % new max, min unchanged meanvar_minmax(NewEsp, NewVar, Min, X, H, Next); X < Min -> % new min, max unchanged meanvar_minmax(NewEsp, NewVar, X, Max, H, Next); true -> meanvar_minmax(NewEsp, NewVar, Min, Max, H, Next) end. %% compute the variance of a list variance([]) -> 0; variance(H) -> {_Mean, Var, I} = meanvar(0, 0, H, 0), Var/I. stdvar(H) -> math:sqrt(variance(H)). tsung-1.8.0/src/tsung/ts_ssl_session_cache.erl0000644000201100017670000000542314377756736021232 0ustar nniclausdream%%% %%% Copyright 2014 (c) Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 15 avril 2014 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% -module(ts_ssl_session_cache). -vc('$Id: ts_ssl_session_cache.erl,v 0.0 2014/04/15 07:28:58 nniclaus Exp $ '). -author('nicolas@niclux.org'). -behaviour(ssl_session_cache_api). -include("ts_macros.hrl"). -export([init/1, terminate/1, lookup/2, update/3, delete/2, foldl/3, select_session/2, size/1]). -ifndef(new_time_api). % otp < R18 -define(SELECT(Cache,PartialKey), ets:select(Cache, [{{{PartialKey,'$1'}, '$2'},[],['$$']}])). -else. -define(SELECT(Cache,PartialKey), ets:select(Cache, [{{{PartialKey,'_'}, '$1'},[],['$1']}])). -endif. %%-------------------------------------------------------------------- %% Description: Return table reference. Called by ssl_manager process. %%-------------------------------------------------------------------- init(_) -> ets:new(cache_name(), [protected]). terminate(Cache) -> ets:delete(Cache). lookup(Cache, Key) -> ?DebugF("Lookup key ~p from session cache",[Key]), case ets:lookup(Cache, Key) of [{Key, Session}] -> Session; [] -> undefined end. update(Cache, Key, Session) -> case application:get_env(tsung,ssl_session_cache) of {ok, 0} -> ?Debug("SSL session cache is disabled, skip"); _ -> ?DebugF("SSL update entry ~p ~p",[Key,Session]), ets:insert(Cache, {Key, Session}) end. delete(Cache, Key) -> ?DebugF("Delete key from session cache ~p",[Key]), ets:delete(Cache, Key). foldl(Fun, Acc0, Cache) -> ets:foldl(Fun, Acc0, Cache). select_session(Cache, PartialKey) -> ?DebugF("SSL cache select ~p",[PartialKey]), ?SELECT(Cache,PartialKey). size(Cache) -> ets:info(Cache, size). %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- cache_name() -> ts_ssl_session_cache. tsung-1.8.0/src/tsung/ts_ssl.erl0000644000201100017670000000506114377756736016342 0ustar nniclausdream-module(ts_ssl). -export([ connect/2, connect/3, connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). protocol_options(Proto=#proto_opts{disable_sni=true}) -> [{server_name_indication, disable}] ++ protocol_options(Proto#proto_opts{disable_sni=false}); protocol_options(Proto=#proto_opts{ip_transparent = true }) -> Opts= [{raw,0,19,<<1:32/native>>} ] ++ protocol_options(Proto#proto_opts{ip_transparent=false}), ?DebugF("SSL Real opts: ~p ~n", [Opts]), Opts; protocol_options(Proto=#proto_opts{ip_bind_address_no_port = true }) -> Opts= [{raw,0,24,<<1:32/native>>} ] ++ protocol_options(Proto#proto_opts{ip_bind_address_no_port=false}), ?DebugF("SSL Real opts: ~p ~n", [Opts]), Opts; protocol_options(#proto_opts{ssl_versions=Versions, ssl_ciphers=Ciphers, certificate = Cert, is_first_connect = First, reuse_sessions =Reuse}) when First or not Reuse-> [binary, {active, once}, {reuse_sessions, false} ] ++ Cert ++ set_ciphers(Ciphers) ++ set_versions(Versions); protocol_options(#proto_opts{ssl_versions=Versions, ssl_ciphers=Ciphers, certificate = Cert}) -> [binary, {active, once}] ++ Cert ++ set_ciphers(Ciphers) ++ set_versions(Versions). set_ciphers(negotiate)-> []; set_ciphers(Ciphers) -> [{ciphers, Ciphers}]. set_versions(negotiate)-> []; set_versions(Versions) -> [{versions, Versions}]. %% -> {ok, Socket} connect(Host, Port, Opts) when is_list(Host) -> connect(Host, Port, opts_to_tcp_opts(Opts), infinity); connect(Socket, Opts, ConnectTimeout) -> ssl:connect(Socket, opts_to_tcp_opts(Opts), ConnectTimeout). connect(Host, Port, Opts, ConnectTimeout) -> ssl:connect(Host, Port, opts_to_tcp_opts(Opts), ConnectTimeout). connect(Socket, Opts) -> connect(Socket, Opts, infinity). opts_to_tcp_opts(Opts) -> Opts. %% send/3 -> ok | {error, Reason} send(Socket, Data, _Opts) -> ssl:send(Socket, Data). close(none) -> ok; close(Socket) -> ssl:close(Socket). % set_opts/2 -> socket() set_opts(none, _Opts) -> none; set_opts(Socket, Opts) -> ssl:setopts(Socket, Opts), Socket. normalize_incomming_data(Socket, {ssl, Socket, Data}) -> {gen_ts_transport, Socket, Data}; normalize_incomming_data(Socket, {ssl_closed, Socket}) -> {gen_ts_transport, Socket, closed}; normalize_incomming_data(Socket, {ssl_error, Socket, Error}) -> {gen_ts_transport, Socket, error, Error}; normalize_incomming_data(_Socket, X) -> X. %%Other, non gen_tcp packet. tsung-1.8.0/src/tsung/ts_ssl6.erl0000644000201100017670000000440214377756736016426 0ustar nniclausdream%%% %%% Copyright 2012 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 7 sep 2012 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_ssl6). -export([ connect/2, connect/3, connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). protocol_options(Opts) -> [inet6]++ts_ssl:protocol_options(Opts). %% -> {ok, Socket} connect(Host, Port, Opts) when is_list(Host) -> connect(Host, Port, Opts, infinity); connect(Socket, Opts, ConnectTimeout) -> ssl:connect(Socket, Opts, ConnectTimeout). connect(Host, Port, Opts, ConnectTimeout) -> ssl:connect(Host, Port, Opts, ConnectTimeout). connect(Socket, Opts) -> connect(Socket, Opts, infinity). %% send/3 -> ok | {error, Reason} send(Socket, Data, _Opts) -> ssl:send(Socket, Data). close(none) -> ok; close(Socket) -> ssl:close(Socket). % set_opts/2 -> socket() set_opts(none, _Opts) -> none; set_opts(Socket, Opts) -> ssl:setopts(Socket, Opts), Socket. normalize_incomming_data(Socket, Data) -> ts_ssl:normalize_incomming_data(Socket,Data). tsung-1.8.0/src/tsung/ts_shell.erl0000644000201100017670000001337514377756736016657 0ustar nniclausdream%%% %%% Copyright 2009 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 20 août 2009 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_shell). -vc('$Id: ts_erlang.erl,v 0.0 2009/08/20 16:31:58 nniclaus Exp $ '). -author('nniclaus@sophia.inria.fr'). -behaviour(ts_plugin). -include("ts_profile.hrl"). -include("ts_shell.hrl"). -include_lib("kernel/include/file.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, dump/2, parse/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). %%==================================================================== %% Data Types %%==================================================================== %% @type dyndata() = #dyndata{proto=ProtoData::term(),dynvars=list()}. %% Dynamic data structure %% @end %% @type server() = {Host::tuple(),Port::integer(),Protocol::atom()}. %% Host/Port/Protocol tuple %% @end %% @type param() = {dyndata(), server()}. %% Dynamic data structure %% @end %% @type hostdata() = {Host::tuple(),Port::integer()}. %% Host/Port pair %% @end %% @type client_data() = binary() | closed. %% Data passed to a protocol implementation is either a binary or the %% atom closed indicating that the server closed the tcp connection. %% @end %%==================================================================== %% API %%==================================================================== parse_config(El,Config) -> ts_config_shell:parse_config(El, Config). %% @spec session_defaults() -> {ok, Persistent} | {ok, Persistent, Bidi} %% Persistent = bool() %% Bidi = bool() %% @doc Default parameters for sessions of this protocol. Persistent %% is true if connections are preserved after the underlying tcp %% connection closes. Bidi should be true for bidirectional protocols %% where the protocol module needs to reply to data sent from the %% server. @end session_defaults() -> {ok, true}. % not relevant for erlang type (?). %% @spec new_session() -> State::term() %% @doc Initialises the state for a new protocol session. %% @end new_session() -> #shell_sess{}. %% @spec decode_buffer(Buffer::binary(),Session::record(shell)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#shell{}) -> Buffer. % nothing to do for shell %% @spec add_dynparams(Subst, dyndata(), param(), hostdata()) -> {dyndata(), server()} | dyndata() %% Subst = term() %% @doc Updates the dynamic request data structure created by %% {@link ts_protocol:init_dynparams/0. init_dynparams/0}. %% @end add_dynparams(false, {_DynVars, Session}, Param, HostData) -> add_dynparams(Session, Param, HostData); add_dynparams(true, {DynVars, Session}, Param, HostData) -> NewParam = subst(Param, DynVars), add_dynparams(Session,NewParam, HostData). add_dynparams(#shell_sess{}, Param, _HostData) -> Param. %%---------------------------------------------------------------------- %% @spec subst(record(shell), term()) -> record(shell) %% @doc Replace on the fly dynamic element of the request. @end %%---------------------------------------------------------------------- subst(Req=#shell{command=Cmd,args=Args}, DynVars) -> Req#shell{command=ts_search:subst(Cmd,DynVars),args=ts_search:subst(Args,DynVars)}. dump(A,B) -> ts_plugin:dump(A,B). %% @spec parse(Data::client_data(), State) -> {NewState, Opts, Close} %% State = #state_rcv{} %% Opts = proplist() %% Close = bool() %% @doc %% Opts is a list of inet:setopts socket options. Don't change the %% active/passive mode here as tsung will set {active,once} before %% your options. %% Setting Close to true will cause tsung to close the connection to %% the server. %% @end parse({os, cmd, _Args, Res},State) when is_list(Res)-> {State#state_rcv{ack_done=true,datasize=length(Res)}, [], false}; parse({os, cmd, _Args, Res},State) -> {State#state_rcv{ack_done=true,datasize=size(term_to_binary(Res))}, [], false}. %% @spec parse_bidi(Data, State) -> {nodata, NewState} | {Data, NewState} %% Data = client_data() %% NewState = term() %% State = term() %% @doc Parse a block of data from the server. No reply will be sent %% if the return value is nodata, otherwise the Data binary will be %% sent back to the server immediately. %% @end parse_bidi(_Data, _State) -> erlang:error(dummy_implementation). %% @spec get_message(record(shell),record(state_rcv)) -> {Message::term(),record(state_rcv)} %% @doc Creates a new message to send to the connected server. %% @end get_message(#shell{command=Cmd, args=Args},#state_rcv{session=S}) -> Msg=Cmd++" "++Args , {{os, cmd, [Msg], length(Msg) } , S}. tsung-1.8.0/src/tsung/ts_session_cache.erl0000644000201100017670000002033514377756736020350 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_session_cache.erl %%% Author : Nicolas Niclausse %%% Description : cache sessions request from ts_config_server %%% %%% Created : 2 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_session_cache). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% External exports -export([start/0, get_req/2, get_user_agent/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, { table, % ets table hit =0.0, % number of hits total=0.0 % total number of requests }). -define(DUMP_STATS_INTERVAL, 500). % in milliseconds -include("ts_macros.hrl"). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link/0 %% Description: Starts the server %%-------------------------------------------------------------------- start() -> ?LOG("Starting~n",?INFO), gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% Function: get_req/2 %% Description: get next request from session 'Id' %%-------------------------------------------------------------------- get_req(Id, Count)-> gen_server:call(?MODULE,{get_req, Id, Count}). %%-------------------------------------------------------------------- %% Function: get_user_agent/0 %%-------------------------------------------------------------------- get_user_agent()-> gen_server:call(?MODULE,{get_user_agent}). %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init([]) -> Table = ets:new(sessiontable, [set, private]), {ok, #state{table=Table}}. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- %% get Nth request from given session Id handle_call({get_req, Id, N}, _From, State) -> Tab = State#state.table, Total = State#state.total+1, ?DebugF("look for ~p th request in session ~p for ~p~n",[N,Id,_From]), case ets:lookup(Tab, {Id, N}) of [{_Key, Session}] -> Hit = State#state.hit+1, ?DebugF("ok, found in cache for ~p~n",[_From]), ?DebugF("hitrate is ~.3f~n",[100.0*Hit/Total]), {reply, Session, State#state{hit= Hit, total = Total}}; [] -> %% no match, ask the config_server ?DebugF("not found in cache (~p th request in session ~p for ~p)~n",[N,Id,_From]), case catch ts_config_server:get_req(Id, N) of {'EXIT',Reason} -> {reply, {error, Reason}, State}; Reply -> %% cache the response FIXME: handle bad response ? ets:insert(Tab, {{Id, N}, Reply}), {reply, Reply, State#state{total = Total}} end; Other -> %% ?LOGF("error ! (~p)~n",[Other],?WARN), {reply, {error, Other}, State} end; handle_call({get_user_agent}, _From, State) -> Tab = State#state.table, case ets:lookup(Tab, {http_user_agent, value}) of [] -> %% no match, ask the config_server ?Debug("user agents not found in cache~n"), UserAgents = ts_config_server:get_user_agents(), %% cache the response FIXME: handle bad response ? ?DebugF("Useragents: got from config_server~p~n",[UserAgents]), ets:insert(Tab, {{http_user_agent, value}, UserAgents}), {ok, Reply} = choose_user_agent(UserAgents), {reply, Reply, State}; [{_, [{_Freq, Value}]}] -> %single user agent defined {reply, Value, State}; [{_, empty }] -> {reply, "tsung", State}; [{_, UserAgents }] when is_list(UserAgents)-> {ok, Reply} = choose_user_agent(UserAgents), {reply, Reply, State} end; handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(Reason, _State) -> ?LOGF("Die ! (~p)~n",[Reason],?ERR), ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- choose_user_agent(empty) -> {ok, "tsung"}; choose_user_agent([{_P, Val}]) -> {ok, Val}; choose_user_agent(UserAgents) -> choose_user_agent(UserAgents, random:uniform(100),0). choose_user_agent([{P, Val} | _],Rand, Cur) when Rand =< P+Cur-> {ok, Val}; choose_user_agent([{P, _Val} | SList], Rand, Cur) -> choose_user_agent(SList, Rand, Cur+P). tsung-1.8.0/src/tsung/ts_server_websocket_ssl.erl0000644000201100017670000001551114377756736021777 0ustar nniclausdream%%% %%% Copyright 2010 © ProcessOne %%% %%% Author : Eric Cestari %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module (ts_server_websocket_ssl). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_websocket.hrl"). -record(state, {parent, socket = none, accept, host, port, path, opts, version, origin, frame, buffer = <<>>, state = not_connected, subprotos = []}). -record(ws_config, {path, version = "13", frame, origin, subprotos}). protocol_options(#proto_opts{tcp_rcv_size = Rcv, tcp_snd_size = Snd, websocket_path = Path, websocket_frame = Frame, websocket_origin = Origin, websocket_subprotocols = SubProtocols}) -> [#ws_config{path = Path, frame = Frame, origin = Origin, subprotos = SubProtocols}, binary, {active, once}, {recbuf, Rcv}, {sndbuf, Snd}, {keepalive, true} %% FIXME: should be an option ]. connect(Host, Port, Opts, Timeout) -> Parent = self(), [WSConfig | TcpOpts] = Opts, Path = WSConfig#ws_config.path, Version = WSConfig#ws_config.version, Frame = WSConfig#ws_config.frame, Protocol = WSConfig#ws_config.subprotos, Origin = WSConfig#ws_config.origin, case ssl:connect(Host, Port, opts_to_tcp_opts(TcpOpts),Timeout) of {ok, Socket} -> Pid = spawn_link( fun() -> loop(#state{parent = Parent, host = Host, port = Port, subprotos = Protocol, origin = Origin, opts = TcpOpts, path = Path, version = Version, frame = Frame, socket = Socket}) end), ssl:controlling_process(Socket, Pid), ssl:setopts(Socket, [{active, once}]), {ok, Pid}; Ret -> Ret end. loop(#state{socket = Socket, host = Host, path = Path, version = Version, subprotos = SubProtocol, origin = Origin, state = not_connected} = State)-> Headers = "", {Handshake, Accept} = websocket:get_handshake(Host, Path, SubProtocol, Version, Origin, Headers), ssl:send(Socket, Handshake), loop(State#state{socket = Socket, accept = Accept, state = waiting_handshake}); loop(#state{parent = Parent, socket = Socket, accept = Accept, state = waiting_handshake} = State)-> receive {ssl, Socket, Data}-> CheckResult = websocket:check_handshake(Data, Accept), case CheckResult of ok -> ?Debug("handshake success: ~n"), ssl:setopts(Socket, [{active, once}]), loop(State#state{state = connected}); {error, Reason} -> ?DebugF("handshake fail: ~p~n", [Reason]), Parent ! {gen_ts_transport, self(), error, Reason} end; {ssl_closed, Socket}-> ?LOGF("tcp closed:~p~n", [Socket], ?ERR), Parent ! {gen_ts_transport, self(), closed}; {ssl_error, Socket, Error}-> ?LOGF("tcp error:~p~n", [Socket], ?ERR), Parent ! {gen_ts_transport, self(), error, Error} end; loop(#state{parent = Parent, socket = Socket, state = connected, buffer = Buffer, frame = Frame} = State)-> receive {send, Data, Ref} -> EncodedData = case Frame of "text" -> websocket:encode_text(Data); _ -> websocket:encode_binary(Data) end, ssl:send(Socket, EncodedData), Parent ! {ok, Ref}, loop(State); close -> EncodedData = websocket:encode_close(<<"close">>), ssl:send(Socket, EncodedData), ssl:close(Socket); {set_opts, Opts} -> ssl:setopts(Socket, Opts), loop(State); {ssl, Socket, Data}-> case websocket:decode(<>) of more -> ?DebugF("receive incomplete from server: ~p~n", [Data]), loop(State#state{buffer = <>}); {?OP_CLOSE, _Reason, _} -> ?DebugF("receive close from server: ~p~n", [_Reason]), Parent ! {gen_ts_transport, self(), closed}; {_Opcode, Payload, Left} -> ?DebugF("receive from server: ~p ~p~n", [_Opcode, Payload]), Parent ! {gen_ts_transport, self(), Payload}, loop(State#state{buffer = Left}) end; {ssl_closed, Socket}-> Parent ! {gen_ts_transport, self(), closed}; {ssl_error, Socket, Error}-> Parent ! {gen_ts_transport, self(), error, Error}; E -> ?LOGF("Message:~p~n", [E], ?WARN) end. opts_to_tcp_opts(Opts) -> Opts. %% send/3 -> ok | {error, Reason} send(Socket, Data, _Opts) -> ?DebugF("sending to server: ~p~n",[Data]), Ref = make_ref(), Socket ! {send, Data, Ref}, MonitorRef = erlang:monitor(process,Socket), receive {'DOWN', MonitorRef, _Type, _Object, _Info} -> {error, no_ws_connection}; {ok, Ref} -> erlang:demonitor(MonitorRef), ok after 30000 -> erlang:demonitor(MonitorRef), {error, timeout} end. close(none) -> ok; close(Socket) -> Socket ! close. %% set_opts/2 -> socket() set_opts(Socket, Opts) -> Socket ! {set_opts, Opts}, Socket. normalize_incomming_data(_Socket, X) -> %% nothing to do here, ts_websocket uses a special process to handle %%http requests,the incoming data is already delivered to %%ts_client as {gen_ts_transport, ..} instead of gen_tcp | ssl X. tsung-1.8.0/src/tsung/ts_server_websocket.erl0000644000201100017670000001542214377756736021117 0ustar nniclausdream%%% %%% Copyright 2010 © ProcessOne %%% %%% Author : Eric Cestari %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module (ts_server_websocket). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_websocket.hrl"). -record(state, {parent, socket = none, accept, host, port, path, opts, version, frame, buffer = <<>>, state = not_connected, subprotos = []}). -record(ws_config, {path, version = "13", frame, subprotos}). protocol_options(#proto_opts{tcp_rcv_size = Rcv, tcp_snd_size = Snd, websocket_path = Path, websocket_frame = Frame, websocket_subprotocols = SubProtocols}) -> [#ws_config{path = Path, frame = Frame, subprotos = SubProtocols}, binary, {active, once}, {recbuf, Rcv}, {sndbuf, Snd}, {keepalive, true} %% FIXME: should be an option ]. connect(Host, Port, Opts, Timeout) -> Parent = self(), [WSConfig | TcpOpts] = Opts, Path = WSConfig#ws_config.path, Version = WSConfig#ws_config.version, Frame = WSConfig#ws_config.frame, Protocol = WSConfig#ws_config.subprotos, case gen_tcp:connect(Host, Port, opts_to_tcp_opts(TcpOpts),Timeout) of {ok, Socket} -> Pid = spawn_link( fun() -> loop(#state{parent = Parent, host = Host, port = Port, subprotos = Protocol, opts = TcpOpts, path = Path, version = Version, frame = Frame, socket = Socket}) end), gen_tcp:controlling_process(Socket, Pid), inet:setopts(Socket, [{active, once}]), {ok, Pid}; Ret -> Ret end. loop(#state{socket = Socket, host = Host, path = Path, version = Version, subprotos = SubProtocol, state = not_connected} = State)-> Origin = "", % FIXME: can we make it configurable ? Headers = "", {Handshake, Accept} = websocket:get_handshake(Host, Path, SubProtocol, Version, Origin, Headers), gen_tcp:send(Socket, Handshake), loop(State#state{socket = Socket, accept = Accept, state = waiting_handshake}); loop(#state{parent = Parent, socket = Socket, accept = Accept, state = waiting_handshake} = State)-> receive {tcp, Socket, Data}-> CheckResult = websocket:check_handshake(Data, Accept), case CheckResult of ok -> ?Debug("handshake success: ~n"), inet:setopts(Socket, [{active, once}]), loop(State#state{state = connected}); {error, Reason} -> ?DebugF("handshake fail: ~p~n", [Reason]), Parent ! {gen_ts_transport, self(), error, Reason} end; {tcp_closed, Socket}-> ?LOGF("tcp closed:~p~n", [Socket], ?ERR), Parent ! {gen_ts_transport, self(), closed}; {tcp_error, Socket, Error}-> ?LOGF("tcp error:~p~n", [Socket], ?ERR), Parent ! {gen_ts_transport, self(), error, Error} end; loop(#state{parent = Parent, socket = Socket, state = connected, buffer = Buffer, frame = Frame} = State)-> receive {send, Data, Ref} -> EncodedData = case Frame of "text" -> websocket:encode_text(Data); _ -> websocket:encode_binary(Data) end, gen_tcp:send(Socket, EncodedData), Parent ! {ok, Ref}, loop(State); close -> EncodedData = websocket:encode_close(<<"close">>), gen_tcp:send(Socket, EncodedData), gen_tcp:close(Socket); {set_opts, Opts} -> inet:setopts(Socket, Opts), loop(State); {tcp, Socket, Data}-> case websocket:decode(<>) of more -> ?DebugF("receive incomplete from server: ~p~n", [Data]), loop(State#state{buffer = <>}); {?OP_CLOSE, _Reason, _} -> ?DebugF("receive close from server: ~p~n", [_Reason]), Parent ! {gen_ts_transport, self(), closed}; {_Opcode, Payload, Left} -> ?DebugF("receive from server: ~p ~p~n", [_Opcode, Payload]), Parent ! {gen_ts_transport, self(), Payload}, loop(State#state{buffer = Left}) end; {tcp_closed, Socket}-> Parent ! {gen_ts_transport, self(), closed}; {tcp_error, Socket, Error}-> Parent ! {gen_ts_transport, self(), error, Error}; E -> ?LOGF("Message:~p~n", [E], ?WARN) end. opts_to_tcp_opts(Opts) -> Opts. %% send/3 -> ok | {error, Reason} send(Socket, Data, _Opts) -> ?DebugF("sending to server: ~p~n",[Data]), Ref = make_ref(), Socket ! {send, Data, Ref}, MonitorRef = erlang:monitor(process,Socket), receive {'DOWN', MonitorRef, _Type, _Object, _Info} -> {error, no_ws_connection}; {ok, Ref} -> erlang:demonitor(MonitorRef), ok after 30000 -> erlang:demonitor(MonitorRef), {error, timeout} end. close(none) -> ok; close(Socket) -> Socket ! close. %% set_opts/2 -> socket() set_opts(Socket, Opts) -> Socket ! {set_opts, Opts}, Socket. normalize_incomming_data(_Socket, X) -> %% nothing to do here, ts_websocket uses a special process to handle %%http requests,the incoming data is already delivered to %%ts_client as {gen_ts_transport, ..} instead of gen_tcp | ssl X. tsung-1.8.0/src/tsung/ts_search.erl0000644000201100017670000005361614377756736017017 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% File : ts_search.erl %%% Author : Mickael Remond %%% Description : Add dynamic / Differentiated parameters in tsung %%% request and response %%% The function subst is intended to be called for each %%% relevant field in ts_protocol implementation. %%% Created : 22 Mar 2004 by Mickael Remond %%% Nicolas Niclausse: add dynamic variable and matching -module(ts_search). -vc('$Id$ '). -export([subst/2, match/5, parse_dynvar/2, parse_dynvar/3]). -include("ts_macros.hrl"). -include("ts_profile.hrl"). %% @type dynvar() = {Key::atom(), Value::string()} | []. %% @type dynvars() = [dynvar()] %% ---------------------------------------------------------------------- %% @spec subst(Data::term(), DynVar::dynvars() ) -> term() %% @doc search into a given string and replace %%Mod:Fun%% (resp %% %%__Variable%%) strings by the result of the call to %% Mod:Fun({Pid, DynVars }) (resp the value of the variable) where Pid %% is the Pid of the client. The substitution tag are %% intended to be used in tsung.xml scenarii files. %% @end %% ---------------------------------------------------------------------- subst(Int, _DynVar) when is_integer(Int) -> Int; subst(Atom, _DynVar) when is_atom(Atom) -> Atom; subst(Binary, DynVar) when is_binary(Binary) -> list_to_binary(subst(binary_to_list(Binary), DynVar)); subst(String, DynVar) -> subst(String, DynVar, []). subst([], _DynVar, Acc) -> lists:reverse(Acc); subst([$%,$%,$_|Rest], DynVar, Acc) -> extract_variable(Rest, DynVar, Acc, []); subst([$%,$%|Rest], DynVar, Acc) -> extract_module(Rest, DynVar, Acc, []); subst([H|Tail], DynVar, Acc) -> subst(Tail, DynVar, [H|Acc]). %% Search for the module string in the subst markup extract_module([],_DynVar, Acc,_) -> lists:reverse(Acc); extract_module([$:|Tail],DynVar, Acc, Mod) -> ?DebugF("found module name: ~p~n",[lists:reverse(Mod)]), extract_function(Tail,DynVar, Acc,lists:reverse(Mod),[]); extract_module([H|Tail],DynVar, Acc, Mod) -> extract_module(Tail,DynVar, Acc,[H|Mod]). %% Search for the module string in the subst markup extract_variable([],_DynVar,Acc,_) -> lists:reverse(Acc); extract_variable([$%,$%|Tail], DynVar, Acc, Var) -> VarName = list_to_atom(lists:reverse(Var)), case ts_dynvars:lookup(VarName,DynVar) of {ok, ResultTmp} -> Result=ts_utils:term_to_list(ResultTmp), ?DebugF("found value ~p for name ~p~n",[Result,VarName]), subst(Tail, DynVar,lists:reverse(Result) ++ Acc); false -> ?LOGF("DynVar: no value found for var ~p~n",[VarName],?WARN), subst(Tail, DynVar,lists:reverse("undefined") ++ Acc) end; extract_variable([H|Tail],DynVar,Acc,Mod) -> extract_variable(Tail,DynVar,Acc,[H|Mod]). %% Search for the function string and do the real substitution before %% keeping on the parsing extract_function([], _DynVar, Acc, _Mod, _Fun) -> lists:reverse(Acc); extract_function([$%,$%|Tail], DynVar, Acc, Mod, Fun) -> ?DebugF("found function name: ~p~n",[lists:reverse(Fun)]), Module = list_to_atom(Mod), Function = list_to_atom(lists:reverse(Fun)), Result = case Module:Function({self(), DynVar }) of Int when is_integer(Int) -> lists:reverse(integer_to_list(Int)); Str when is_list(Str) -> Str; _Val -> ?LOGF("extract fun:bad result ~p~n",[_Val],?WARN), [] end, subst(Tail, DynVar, lists:reverse(Result) ++ Acc); extract_function([H|Tail],DynVar, Acc, Mod, Fun) -> extract_function(Tail, DynVar, Acc, Mod, [H|Fun]). %%---------------------------------------------------------------------- %% @spec match(Match::#match{}, Data::binary() | list, %% {Counts::integer(), Max::integer(), SessionId::integer(), UserId::integer()}, %% Dynvars::term(), Transactions::list() ) -> Count::integer() %% @doc search for regexp in Data; send result to ts_mon %% @end %%---------------------------------------------------------------------- match([], _Data, {Count, _MaxC, _SessionId, _UserId}, _DynVars, _Tr) -> Count; match([Match=#match{'skip_headers'=http}|Tail], Data, Counts, DynVars, Tr) when is_binary(Data)-> %% keep http body only case re:run(Data,"\\r\\n\\r\\n(.*)",[{capture,all_but_first,binary},dotall]) of {match,[NewData]} -> match([Match#match{'skip_headers'=no}|Tail], NewData, Counts, DynVars, Tr); _ -> ?LOGF("Skip http headers failure, data was: ~p ~n",[Data], ?ERR), match([Match#match{'skip_headers'=no}|Tail], Data, Counts, DynVars, Tr) end; match([Match=#match{'apply_to_content'=undefined}|Tail], Data, Counts,DynVars,Tr) -> ?DebugF("Matching Data size ~p; apply undefined~n",[ts_utils:size_or_length(Data)]), match([Match|Tail], Data, Counts, [],DynVars, Tr); match([Match=#match{'apply_to_content'={Module,Fun}}|Tail], Data, Counts,DynVars,Tr) -> ?DebugF("Matching Data size ~p; apply ~p:~p~n",[ts_utils:size_or_length(Data),Module,Fun]), NewData = Module:Fun(Data), ?DebugF("Match: apply result =~p~n",[NewData]), match([Match|Tail], NewData, Counts, [],DynVars, Tr). %% @spec match(Match::#match{}, Data::binary() | list(), Count::tuple(), %% Stats::list(), DynVars::term(), Transaction::atom()) -> Count::integer() match([], _Data, {Count,_, _,_}, Stats, _, _) -> %% all matches done, add stats, and return Count unchanged (continue) ts_mon_cache:add(Stats), Count; match([Match=#match{regexp=RawRegExp,subst=Subst, do=Action, 'when'=When} |Tail], Data,Counts,Stats,DynVars, Tr)-> RegExp = case Subst of true -> subst(RawRegExp, DynVars); _ -> RawRegExp end, ?DebugF("RegExp was ~p and now is ~p after substitution (~p)~n",[RawRegExp,RegExp,Subst]), case re:run(Data, RegExp) of {When,_} -> ?LOGF("Ok Match (regexp=~p) do=~p~n",[RegExp,Action], ?INFO), case Action of Act when Act =:= 'continue'; Act =:= 'log'; Act =:= 'dump' -> setcount(Match, Counts, [{count, match}| Stats], Data, Tr), match(Tail, Data, Counts, Stats,DynVars, Tr); _ -> setcount(Match, Counts, [{count, match}| Stats], Data, Tr) end; When -> % nomatch ?LOGF("Bad Match (regexp=~p) do=~p~n",[RegExp, Action], ?INFO), case Action of Act when Act =:= 'continue'; Act =:= 'log'; Act =:= 'dump' -> setcount(Match, Counts, [{count, nomatch}| Stats], Data, Tr), match(Tail, Data, Counts, Stats,DynVars, Tr); _ -> setcount(Match, Counts, [{count, nomatch}| Stats], Data, Tr) end; {match,_} -> % match but when=nomatch ?LOGF("Ok Match (regexp=~p)~n",[RegExp], ?INFO), case Action of loop -> put(loop_count, 0); restart -> put(restart_count, 0); _ -> ok end, match(Tail, Data, Counts, [{count, match} | Stats],DynVars, Tr); nomatch -> % nomatch but when=match ?LOGF("Bad Match (regexp=~p)~n",[RegExp], ?INFO), case Action of loop -> put(loop_count, 0); restart -> put(restart_count, 0); _ -> ok end, match(Tail, Data, Counts,[{count, nomatch} | Stats],DynVars,Tr) end. %%---------------------------------------------------------------------- %% Func: setcount/3 %% Args: #match, Counts, Stats %% Update the request counter after a match: %% - if loop is true, we must start again the same request, so add 1 to count %% - if restart is true, we must start again the whole session, set count to MaxCount %% - if stop is true, set count to 0 %%---------------------------------------------------------------------- setcount(#match{do=continue}, {Count, _MaxC, _SessionId, _UserId}, Stats,_,_)-> ts_mon_cache:add(Stats), Count; setcount(#match{do=log, name=Name}, {Count, MaxC, SessionId, UserId}, Stats,_,Tr)-> ts_mon_cache:add_match(Stats,{UserId,SessionId,MaxC-Count,Tr, Name}), Count; setcount(#match{do=dump, name=Name}, {Count, MaxC, SessionId, UserId}, Stats, Data, Tr)-> ts_mon_cache:add_match(Stats,{UserId,SessionId,MaxC-Count, Data, Tr, Name}), Count; setcount(#match{do=restart, max_restart=MaxRestart, name=Name}, {Count, MaxC,SessionId,UserId}, Stats,_, Tr)-> CurRestart = get(restart_count), Ids={UserId,SessionId,MaxC-Count,Tr,Name}, ?LOGF("Restart on (no)match ~p~n",[CurRestart], ?INFO), case CurRestart of undefined -> put(restart_count,1), ts_mon_cache:add_match([{count, match_restart} | Stats],Ids), MaxC ; Val when Val >= MaxRestart -> ?LOG("Max restart reached, abort ! ~n", ?WARN), ts_mon_cache:add_match([{count, match_restart_abort} | Stats],Ids), 0; Val -> put(restart_count, Val +1), ts_mon_cache:add_match([{count, match_restart} | Stats],Ids), MaxC end; setcount(#match{do=loop,loop_back=Back,max_loop=MaxLoop,sleep_loop=Sleep},{Count,_MaxC,_SessionId,_UserId},Stats,_,_)-> CurLoop = get(loop_count), ?LOGF("Loop on (no)match ~p~n",[CurLoop], ?INFO), ts_mon_cache:add([{count, match_loop} | Stats]), case CurLoop of undefined -> put(loop_count,1), timer:sleep(Sleep), Count +1 + Back ; Val when Val >= MaxLoop -> ?LOG("Max Loop reached, abort loop on request! ~n", ?WARN), put(loop_count, 0), Count; Val -> put(loop_count, Val +1), timer:sleep(Sleep), Count + 1 + Back end; setcount(#match{do=abort,name=Name}, {Count,MaxC,SessionId,UserId}, Stats,_, Tr) -> ts_mon_cache:add_match([{count, match_stop} | Stats],{UserId,SessionId,MaxC-Count,Tr, Name}), 0; setcount(#match{do=abort_test,name=Name}, {Count,MaxC,SessionId,UserId}, Stats,_, Tr) -> ?LOG("OK match, aborting the whole test by request !!!~n", ?EMERG), ts_mon_cache:add_match([{count, match_stop_test} | Stats],{UserId,SessionId,MaxC-Count,Tr, Name}), timer:sleep(?CACHE_DUMP_STATS_INTERVAL), ts_config_server:stop(), 0. %%---------------------------------------------------------------------- %% @spec parse_dynvar(Dynvarspecs::list(), Data::binary | list) -> dynvars() %% @doc Look for dynamic variables in Data %% @end %%---------------------------------------------------------------------- parse_dynvar(Specs, Data) -> parse_dynvar(Specs, Data, ts_dynvars:new()). parse_dynvar([], _Data, DynVars) -> DynVars; parse_dynvar(DynVarSpecs, Data, DynVars) when is_binary(Data) -> ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]), parse_dynvar(DynVarSpecs,Data, undefined,undefined,DynVars); parse_dynvar(DynVarSpecs, {_,_,_,Data}, DynVars) when is_binary(Data) -> ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]), parse_dynvar(DynVarSpecs,Data, undefined,undefined,DynVars); parse_dynvar(DynVarSpecs, {_,_,_,Data}, DynVars) when is_list(Data) -> ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]), parse_dynvar(DynVarSpecs,list_to_binary(Data), undefined,undefined,DynVars); parse_dynvar(DynVarSpecs, _Data, _DynVars) -> ?LOGF("Error while Parsing dyn Variable(~p)~n",[DynVarSpecs],?WARN), ts_dynvars:new(). % parse_dynvar(DynVars,BinaryData,ListData,TreeData,Accum) % ListData and TreeData are lazy computed when needed by % regexp or xpath variables respectively parse_dynvar([],_Binary , _String,_Tree, DynVars) -> DynVars; parse_dynvar(D=[{re,_, _, _}| _],Binary,undefined,Tree,DynVars) -> parse_dynvar(D,Binary,Binary,Tree,DynVars); parse_dynvar([{re,Name,RE}| Tail],Binary,Data,Tree,DynVars) -> parse_dynvar([{re,Name, RE, undefined}| Tail],Binary,Data,Tree,DynVars); parse_dynvar([{re,VarName, RegExp, Apply}| DynVarsSpecs],Binary,Data,Tree,DynVars) -> case re:run(Data, RegExp,[{capture,[1],binary}]) of {match,[Value]} -> ConvValue = apply_fun(Apply,Value), ?LOGF("DynVar (RE): Match (~p=~p) Converted: ~p~n",[VarName, Value, ConvValue], ?INFO), parse_dynvar(DynVarsSpecs,Binary,Data,Tree, ts_dynvars:set(VarName,ConvValue,DynVars)); nomatch -> ?LOGF("Dyn Var (RE): no Match (varname=~p), ~n",[VarName], ?NOTICE), ?LOGF("Regexp was: ~p ~n",[RegExp], ?INFO), parse_dynvar(DynVarsSpecs,Binary,Data,Tree, ts_dynvars:set(VarName,<< >> ,DynVars)) end; parse_dynvar([{header,VarName, HeaderName}| DynVarsSpecs], Binary,String,Tree, DynVars) -> BinHeaders = extract_headers(Binary), Headers = mochiweb_headers:from_binary(BinHeaders), case string:tokens(HeaderName, "/") of [H1] -> V1 = mochiweb_headers:get_value(H1, Headers), ?LOGF("DynVar: Header (~p=~p) ~n",[VarName, V1], ?NOTICE), parse_dynvar(DynVarsSpecs, Binary,String,Tree, ts_dynvars:set(VarName,V1,DynVars)); [H1,SubH] -> Value = case mochiweb_headers:get_value(H1, Headers) of [] -> {ok, Old} = ts_dynvars:lookup(VarName, DynVars, ""), ?LOGF("DynVar: Header ~p not found ; using ~p ~n",[H1, Old], ?NOTICE), Old; undefined -> {ok, Old} = ts_dynvars:lookup(VarName, DynVars, ""), ?LOGF("DynVar: Header ~p not found ; using ~p ~n",[H1, Old], ?NOTICE), Old; SubV when H1 == "www-authenticate" orelse H1 == "authentication-info"-> ?LOGF("DynVar: Found header ~p ~n",[SubV], ?NOTICE), {_, Params} = parse_header(SubV, ","), ?LOGF("DynVar: Parsed subheader ~p ~n",[Params], ?NOTICE), case lists:keyfind(SubH, 1, Params) of false -> {ok, Old} = ts_dynvars:lookup(VarName, DynVars, ""), ?LOGF("DynVar: SubHeader ~p not found ; using ~p ~n",[VarName, Old], ?NOTICE), Old; {_, V} -> ?LOGF("DynVar: SubHeader (~p=~p) ~n",[VarName, V], ?DEB), V end; SubV -> {_, Params}= parse_header(SubV, ";"), case lists:keyfind(SubH, 1, Params) of false -> ?LOGF("DynVar: SubHeader ~p not found ~n",[VarName], ?NOTICE), {ok, Old} = ts_dynvars:lookup(VarName, DynVars, ""), Old; {_, V} -> ?LOGF("DynVar: SubHeader (~p=~p) ~n",[VarName, V], ?INFO), V end end, parse_dynvar(DynVarsSpecs, Binary,String,Tree, ts_dynvars:set(VarName,Value,DynVars)) end; parse_dynvar(D=[{xpath,_VarName, _Expr}| _DynVarsSpecs], Binary,String,undefined,DynVars) -> Body = extract_body(Binary), ToParse = case bit_size(Body) of 0 -> Binary; _ -> Body end, try mochiweb_html:parse(ToParse) of Tree -> parse_dynvar(D,Binary,String,Tree,DynVars) catch Type:Exp -> ?LOGF("Page couldn't be parsed:(~p:~p) ~n Page:~p~n", [Type,Exp,Binary],?NOTICE), ts_mon_cache:add({ count, error_xml_unparsable }), parse_dynvar(D,Binary,String,xpath_error,DynVars) end; parse_dynvar(D=[{jsonpath,_VarName, _Expr, _SubstitutionFlag}| _DynVarsSpecs], Binary,String,undefined,DynVars) -> Body = extract_body(Binary), try mochijson2:decode(Body) of JSON -> ?LOGF("JSON decode: ~p~n", [JSON],?DEB), parse_dynvar(D,Binary,String,JSON,DynVars) catch Type:Exp -> ?LOGF("JSON couldn't be parsed:(~p:~p) ~n Page:~p~n", [Type,Exp,Binary],?NOTICE), ts_mon_cache:add({ count, error_json_unparsable }), parse_dynvar(D,Binary,String,json_error,DynVars) end; parse_dynvar(D=[{pgsql_expr,_VarName, _Expr}| _DynVarsSpecs], Binary,String,undefined,DynVars) -> Pairs=ts_pgsql:to_pairs(Binary), parse_dynvar(D,Binary,String,Pairs,DynVars); parse_dynvar([{xpath,VarName,_Expr}|DynVarsSpecs],Binary,String,xpath_error,DynVars)-> ?LOGF("Couldn't execute XPath: page not parsed (varname=~p)~n", [VarName],?ERR), ts_mon_cache:add({ count, error_xml_not_parsed }), parse_dynvar(DynVarsSpecs, Binary,String,xpath_error,DynVars); parse_dynvar([{jsonpath,VarName,_Expr,_SubstitutionFlag}|DynVarsSpecs],Binary,String,json_error,DynVars)-> ?LOGF("Couldn't execute JSONPath: page not parsed (varname=~p)~n", [VarName],?NOTICE), ts_mon_cache:add({ count, error_json_not_parsed }), parse_dynvar(DynVarsSpecs, Binary,String,json_error,DynVars); parse_dynvar([{pgsql_expr,VarName,_Expr}|DynVarsSpecs],Binary,String,pgsql_error,DynVars)-> ?LOGF("Couldn't decode pgsql expr from PGSQL binary (varname=~p)~n", [VarName],?ERR), parse_dynvar(DynVarsSpecs, Binary,String,json_error,DynVars); parse_dynvar([{xpath,VarName, Expr}| DynVarsSpecs],Binary,String,Tree,DynVars)-> Value = case mochiweb_xpath:execute(Expr,Tree) of [] -> ?LOGF("Dyn Var: no Match (varname=~p), ~n",[VarName],?NOTICE), << >>; Val -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Val],?INFO), Val end, parse_dynvar(DynVarsSpecs, Binary,String,Tree,ts_dynvars:set(VarName,Value,DynVars)); parse_dynvar([{jsonpath,VarName, Expr, SubstitutionFlag}| DynVarsSpecs],Binary,String,JSON,DynVars)-> NewExpr = case SubstitutionFlag of true -> ts_search:subst(Expr, DynVars); false -> Expr end, Values = case ts_utils:jsonpath(NewExpr,JSON) of undefined -> ?LOGF("Dyn Var: no Match (varname=~p), ~n",[VarName],?NOTICE), << >>; {struct, Struct} -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Struct],?INFO), iolist_to_binary(mochijson2:encode({struct, Struct})); Val -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Val],?INFO), Val end, parse_dynvar(DynVarsSpecs, Binary,String,JSON,ts_dynvars:set(VarName,Values,DynVars)); parse_dynvar([{pgsql_expr,VarName, Expr}| DynVarsSpecs],Binary,String,PGSQL,DynVars)-> Values = case ts_pgsql:find_pair(Expr,PGSQL) of undefined -> ?LOGF("Dyn Var: no Match (varname=~p), ~n",[VarName],?NOTICE), << >>; Val -> ?LOGF("Dyn Var: Match (~p=~p), ~n",[VarName,Val],?INFO), Val end, parse_dynvar(DynVarsSpecs, Binary,String,PGSQL,ts_dynvars:set(VarName,Values,DynVars)); parse_dynvar(Args, _Binary,_String,_Tree, _DynVars) -> ?LOGF("Bad args while parsing Dyn Var (~p)~n", [Args], ?ERR), << >>. apply_fun(undefined, Value) -> Value; apply_fun(Fun,Value) -> Fun(Value). extract_body(Data) -> case re:run(Data,"\r\n\r\n(.*)$",[{capture,all_but_first,binary},dotall]) of nomatch -> Data; {match, [Val]} -> Val; _ -> Data end. extract_headers(<<"\r\n",Rest/binary>>) -> Rest; extract_headers(<<_:1/binary,Rest/binary>>) -> extract_headers(Rest); extract_headers(<<>>) -> <<>>. %% Comes from mochiweb_utils.erl ; very slightly adapted. parse_header(String, ";")-> % for Content-Type and friends [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")], {string:to_lower(Type), lists:foldr(fun prepare_headers/2, [], Parts)}; parse_header(String, ",")-> % for Auth [Type | Rest] = [string:strip(S) || S <- string:tokens(String, " ")], Parts = [string:strip(S) || S <- string:tokens(string:join(Rest, " "), ",")], {string:to_lower(Type), lists:foldr(fun prepare_headers/2, [], Parts)}. unquote_header("\"" ++ Rest) -> unquote_header(Rest, []); unquote_header(S) -> S. prepare_headers(S, Acc)-> case lists:splitwith(fun (C) -> C =/= $= end, S) of {"", _} -> %% Skip anything with no name Acc; {_, ""} -> %% Skip anything with no value Acc; {Name, [$\= | Value]} -> [{string:to_lower(string:strip(Name)), unquote_header(string:strip(Value))} | Acc] end. unquote_header("", Acc) -> lists:reverse(Acc); unquote_header("\"", Acc) -> lists:reverse(Acc); unquote_header([$\\, C | Rest], Acc) -> unquote_header(Rest, [C | Acc]); unquote_header([C | Rest], Acc) -> unquote_header(Rest, [C | Acc]). tsung-1.8.0/src/tsung/ts_raw.erl0000644000201100017670000001043414377756736016332 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2004 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% File : ts_jabber.erl %%% Author : Nicolas Niclausse %%% Purpose : %%% Created : 11 Jan 2004 by Nicolas Niclausse -module(ts_raw). -author('nniclausse@hyperion'). -behavior(ts_plugin). -include("ts_profile.hrl"). -include("ts_raw.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, subst/2, parse/2, parse_bidi/2, dump/2, parse_config/2, decode_buffer/2, new_session/0]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session (ack_type and persistent) %% Returns: {ok, true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok,true}. %% @spec decode_buffer(Buffer::binary(),Session::record(raw)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#raw{}) -> Buffer. %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #raw{}. %%---------------------------------------------------------------------- %% Function: get_message/1 %% Purpose: Build a message/request %% Args: #jabber %% Returns: binary %%---------------------------------------------------------------------- get_message(#raw{datasize=Size},S) when is_list(Size) -> get_message(#raw{datasize=list_to_integer(Size)},S); get_message(#raw{datasize=Size},#state_rcv{session=S}) when is_integer(Size), Size > 0 -> BitSize = Size*8, {<< 0:BitSize >>,S} ; get_message(#raw{data=Data},#state_rcv{session=S})-> {list_to_binary(Data),S}. %%---------------------------------------------------------------------- %% Function: parse/3 %% Purpose: Parse the given data and return a new state %% Args: Data (binary) %% State (record) %% Returns: NewState (record) %%---------------------------------------------------------------------- %% no parsing . use only ack parse(_Data, State) -> State. parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data,State). dump(A,B) -> ts_plugin:dump(A,B). %% parse_config(Element, Conf) -> ts_config_raw:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %%---------------------------------------------------------------------- add_dynparams(_,[], Param, _Host) -> Param; add_dynparams(true, {DynVars, _Session}, OldReq, _Host) -> subst(OldReq, DynVars); add_dynparams(_Subst, _DynData, Param, _Host) -> Param. %%---------------------------------------------------------------------- %% Function: subst/1 %%---------------------------------------------------------------------- subst(Req=#raw{datasize=Size,data=Data},DynVars) -> Req#raw{datasize = ts_search:subst(Size, DynVars), data= ts_search:subst(Data, DynVars)}. tsung-1.8.0/src/tsung/ts_plugin.erl0000644000201100017670000000442614377756736017043 0ustar nniclausdream%%% Copyright (C) 2011 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% Created : 3 Mar 2011 by Nicolas Niclausse %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_plugin). -export([dump/2, parse_bidi/2]). -export([behaviour_info/1]). behaviour_info(callbacks) -> [{add_dynparams, 4}, {get_message, 2}, {session_defaults, 0}, {dump, 2}, {parse, 2}, {parse_bidi, 2}, {parse_config, 2}, {decode_buffer, 2}, {new_session, 0}]; behaviour_info(_Other) -> undefined. %% @spec dump(protocol, {Request::term(),Session::term(), Id::integer(), %% Host::string(),DataSize::integer()}) -> ok %% @doc It can be used to send specific data to the current plugin back to ts_mon %% @end dump(_Type,_Data) -> ok. %% @spec parse_bidi(Data::binary(),State::record(state_rcv)) -> %% {NewData::binary()|nodata, NewState::record(state_rcv), think|continue} %% @doc Parse a block of data from the server. No reply will be sent %% if the return value is nodata, otherwise the Data binary will be %% sent back to the server immediately. If the last argument is %% 'think', it will continue to wait; if it's 'continue', it will %% handle the next action (request, thinktime, ...) %% @end parse_bidi(_Data, State) -> {nodata, State, think}. tsung-1.8.0/src/tsung/ts_pgsql.erl0000644000201100017670000003470014377756736016671 0ustar nniclausdream%%% %%% Copyright (C) Nicolas Niclausse 2005 %%% %%% Author : Nicolas Niclausse %%% Created: 6 Nov 2005 by Nicolas Niclausse %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% --------------------------------------------------------------------- %%% Purpose: plugin for postgresql %%% Dependencies: pgsql modules from jungerl (pgsql_proto and pgsql_util) %%% --------------------------------------------------------------------- -module(ts_pgsql). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behavior(ts_plugin). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_pgsql.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, parse/2, parse_bidi/2, dump/2, parse_config/2, to_pairs/1, find_pair/2, decode_buffer/2, new_session/0]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session %% Returns: {ok, ack_type = parse|no_ack|local, persistent = true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true}. %% @spec decode_buffer(Buffer::binary(),Session::record(pgsql)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#pgsql_session{}) -> Buffer. % nothing to do for pgsql %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #pgsql_session{}. %%---------------------------------------------------------------------- %% Function: get_message/21 %% Purpose: Build a message/request , %% Args: record %% Returns: {binary,#pgsql_session} %%---------------------------------------------------------------------- get_message(#pgsql_request{type=connect, database=DB, username=UserName},#state_rcv{session=S}) -> Version = <>, User = pgsql_util:make_pair(user, UserName), Database = pgsql_util:make_pair(database, DB), StartupPacket = <>, PacketSize = 4 + size(StartupPacket), {<>,S#pgsql_session{username=UserName}}; get_message(#pgsql_request{type=sql,sql=Query},#state_rcv{session=S}) -> {pgsql_proto:encode_message(squery, Query),S}; get_message(#pgsql_request{type=close},#state_rcv{session=S}) -> {pgsql_proto:encode_message(terminate, ""),S}; get_message(#pgsql_request{type=authenticate, auth_method={?PG_AUTH_PASSWD, _Salt},passwd=PassString},#state_rcv{session=S}) -> ?LOGF("PGSQL: Must authenticate (passwd= ~p) ~n",[PassString],?DEB), {pgsql_proto:encode_message(pass_plain, PassString),S}; get_message(#pgsql_request{type=authenticate, auth_method= {?PG_AUTH_MD5, Salt},passwd=PassString},#state_rcv{session=S}) -> User=S#pgsql_session.username, ?LOGF("PGSQL: Must authenticate user ~p with md5 (passwd= ~p, salt=~p) ~n", [User,PassString,Salt],?DEB), {pgsql_proto:encode_message(pass_md5, {User,PassString,Salt}),S}; get_message(#pgsql_request{type=authenticate, auth_method=AuthType},#state_rcv{session=S}) -> ?LOGF("PGSQL: Authentication method not implemented ! [~p] ~n",[AuthType],?ERR), {<<>>, S}; get_message(#pgsql_request{type=execute,name_portal=Portal,max_rows=Max},#state_rcv{session=S}) -> {pgsql_proto:encode_message(execute,{Portal,Max}), S}; get_message(#pgsql_request{type=parse,name_prepared=Name,equery=Query, parameters=Params},#state_rcv{session=S}) -> {pgsql_proto:encode_message(parse,{Name,Query,Params}), S}; get_message(#pgsql_request{type=bind,formats=Formats, name_portal=Portal,name_prepared=NPrep, parameters=Params, formats_results=FormatsResults}, #state_rcv{session=S})-> {pgsql_proto:encode_message(bind,{Portal,NPrep,Params,Formats,FormatsResults}), S}; %% describe get_message(#pgsql_request{type=describe, name_portal=Name,name_prepared=undefined}, #state_rcv{session=S})-> {pgsql_proto:encode_message(describe,{portal,Name}), S}; get_message(#pgsql_request{type=describe, name_portal=undefined,name_prepared=Name}, #state_rcv{session=S})-> {pgsql_proto:encode_message(describe,{prepared_statement,Name}), S}; %% sync get_message(#pgsql_request{type=sync},#state_rcv{session=S}) -> {pgsql_proto:encode_message(sync,[]), S}; %% copyfail get_message(#pgsql_request{type=copyfail,equery=Msg},#state_rcv{session=S}) -> {pgsql_proto:encode_message(copyfail,Msg), S}; %% copydone get_message(#pgsql_request{type=copydone},#state_rcv{session=S}) -> {pgsql_proto:encode_message(copydone,<< >> ), S}; %% copy get_message(#pgsql_request{type=copy,equery=Data},#state_rcv{session=S}) -> {pgsql_proto:encode_message(copy,Data), S}; %% flush get_message(#pgsql_request{type=flush},#state_rcv{session=S}) -> {pgsql_proto:encode_message(flush,[]), S}. parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data,State). dump(A,B) -> ts_plugin:dump(A,B). %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: parse the response from the server and keep information %% about the response in State#state_rcv.session %% Args: Data (binary), State (#state_rcv) %% Returns: {NewState, Options for socket (list), Close = true|false} %%---------------------------------------------------------------------- parse(closed, State) -> {State#state_rcv{ack_done = true, datasize=0}, [], true}; %% new response, compute data size (for stats) parse(Data, State=#state_rcv{acc = [], datasize= 0}) -> parse(Data, State#state_rcv{datasize= size(Data)}); parse(Data, State=#state_rcv{acc = [], session=S}) -> case process_head(Data) of {ok, {ready_for_query, idle}, _ } -> {State#state_rcv{ack_done = true},[],false}; {ok, {ready_for_query, transaction}, _ } -> ?Debug("PGSQL: Transaction ~n"), {State#state_rcv{ack_done = true},[],false}; {ok, {ready_for_query, failed_transaction}, _ } -> ?LOG("PGSQL: Failed Transaction ~n",?NOTICE), ts_mon_cache:add({ count, pgsql_failed_transaction }), {State#state_rcv{ack_done = true},[],false}; {ok, {authenticate, {0, _Salt}}, Tail } -> % auth OK, continue to parse resp. parse(Tail, State); {ok, {error_message, ErrMsg}, Tail } -> ts_mon_cache:add({ count, error_pgsql }), ?LOGF("PGSQL: Got Error Msg from postgresql [~p] ~n",[ErrMsg],?NOTICE), case Tail of << >> -> {State#state_rcv{ack_done = false},[],false}; _ -> parse(Tail, State) end; {ok, {authenticate, AuthType}, _ } -> NewS=S#pgsql_session{auth_method=AuthType}, {State#state_rcv{ack_done = true, session=NewS},[],false}; {ok, {copy_response, {_Format,_ColsFormat}},_ } -> ?LOG("PGSQL: Copy response ~n",?DEB), {State#state_rcv{ack_done = true},[],false}; {ok, _Pair, Tail } -> parse(Tail, State); more -> ?LOG("PGSQL: need more data from socket ~n",?DEB), {State#state_rcv{ack_done = false, acc=Data},[],false} end; %% more data, add this to accumulator and parse, update datasize parse(Data, State=#state_rcv{acc=Acc, datasize=DataSize}) -> NewSize= DataSize + size(Data), parse(<< Acc/binary,Data/binary >>, State#state_rcv{acc=[], datasize=NewSize}). %%---------------------------------------------------------------------- %% Function: parse_config/2 %% Purpose: parse tags in the XML config file related to the protocol %% Returns: List %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_pgsql:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %% (this is used for ex. for Cookies in HTTP) %% for postgres, use this to store the auth method and salt %% Args: Subst (true|false), DynData = #dyndata, Param = #myproto_request %% Host = String %% Returns: #pgsql_request %%---------------------------------------------------------------------- add_dynparams(false, {_DynVars,Session}, Param, HostData) -> add_dynparams(Session, Param, HostData); add_dynparams(true, {DynVars,Session}, Param, HostData) -> NewParam = subst(Param, DynVars), add_dynparams(Session,NewParam, HostData). add_dynparams(DynPgsql, Param, _HostData) -> ?DebugF("Dyndata=~p, param=~p~n",[DynPgsql, Param]), Param#pgsql_request{auth_method=DynPgsql#pgsql_session.auth_method, salt=DynPgsql#pgsql_session.salt}. %%---------------------------------------------------------------------- %% Function: subst/2 %% Purpose: Replace on the fly dynamic element of the request. %% Returns: #pgsql_request %%---------------------------------------------------------------------- subst(Req=#pgsql_request{sql=SQL,database=DB,username=User,passwd=Passwd, parameters=Params}, DynVars) -> Req#pgsql_request{sql=ts_search:subst(SQL, DynVars), username=ts_search:subst(User, DynVars), passwd=ts_search:subst(Passwd, DynVars), parameters=case is_list(Params) of true -> lists:map(fun(X)-> ts_search:subst(X, DynVars) end, Params); false -> Params end, database=ts_search:subst(DB, DynVars) }. %%% -- Internal funs -------------------- %%---------------------------------------------------------------------- %% @spec process_head(Bin::binary()) -> {ok, Pair::list(), Rest::binary()} |more %% @doc parse postgresql binary, and return a tuple or more if the %% response is not complete %% ---------------------------------------------------------------------- process_head(<>) -> ?DebugF("PGSQL: received [~p] size=~p Pckt size= ~p ~n",[Code, Size, size(Tail)]), RealSize = Size-4, case RealSize =< size(Tail) of true -> << Packet:RealSize/binary, Data/binary >> = Tail, {ok, Pair} = pgsql_proto:decode_packet(Code, Packet), ?DebugF("PGSQL: data as string: ~p~n",[pgsql_util:to_string(Packet)]), ?LOGF("PGSQL: Pair=~p ~n",[Pair],?DEB), {ok, Pair, Data }; false -> more end; process_head(_) -> more. %%% -- funs related to dyn_variables %% @spec to_pairs(Bin::binary()) -> list() %% @doc transform postgres binary into list of pairs to_pairs(Bin) -> to_pairs(Bin,[]). %% internal fun, with accumulator to_pairs(<< >>, Acc) -> lists:reverse(Acc); to_pairs(<>, Acc) -> RealSize = Size-4, case RealSize =< size(Tail) of true -> << Packet:RealSize/binary, Data/binary >> = Tail, {ok, Pair} = pgsql_proto:decode_packet(Code, Packet), to_pairs(Data, [Pair| Acc] ); false -> %% partial bin, should not happen; anyway send the current accumulated pairs ?LOGF("real size too small, abort ?!~p (Tail was~p)~n",[Acc,Tail], ?NOTICE), lists:reverse(Acc) % end. %% @spec find_pair(Expr::string(), Pairs::list()) -> term() %% @doc Expr: expression like data_row[4][2], Pairs: list of pairs %% extracted by pgsql_proto:decode_packet. %% @end find_pair(Expr,Pairs)-> Fun= fun(A) -> case catch list_to_integer(A) of I when is_integer(I) -> I; _ -> list_to_atom(A) end end, Str=re:replace(Expr,"\\[(\\d+)\\]","\.\\1",[{return,list},global]), Keys=lists:map(Fun, string:tokens(Str,".")), find_pair_real(Keys,Pairs,1). find_pair_real([Key,Row,ColName],Pairs,CurRow) when is_atom(ColName)-> case get_col_id(atom_to_list(ColName),Pairs) of Col when is_integer(Col) -> find_pair_real([Key,Row,Col],Pairs,CurRow); _ -> undefined end; find_pair_real([Key,SameRow,Y,Z],[{Key,Value}|_],SameRow) when is_atom(Key), is_list(Value) -> case lists:nth(Y,Value) of L when is_list(L) -> lists:nth(Z,L); T when is_tuple(T) -> element(Z,T); _ -> undefined end; find_pair_real([Key,Row,Col],[{Key,Val}|_],Row) when is_atom(Key),is_list(Val),is_integer(Col)-> lists:nth(Col,Val); find_pair_real([Key,Row,Col|_],[{Key,Val}|_],Row) when is_atom(Key),is_tuple(Val)-> element(Col,Val); find_pair_real(A=[Key|_],[{Key,_Value}|Pairs],CurRow) -> %same key,different row find_pair_real(A,Pairs,CurRow+1); find_pair_real(Expr,[_|Pairs],Row) ->% not the same key find_pair_real(Expr,Pairs,Row); find_pair_real(_,_,_) -> undefined. get_col_id(ColName,Pairs) -> Desc=proplists:get_value(row_description,Pairs), case lists:keysearch(ColName,1,Desc) of {value,T} -> element(3,T); % column id is the third element of the tuple. false -> undefined end. tsung-1.8.0/src/tsung/ts_mysql.erl0000644000201100017670000002605314377756736016712 0ustar nniclausdream%%% Created : July 2008 by Grégoire Reboul %%% From : ts_pgsql.erl by Nicolas Niclausse %%% Note : Based on erlang-mysql by Magnus Ahltorp & Fredrik Thulin %% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% --------------------------------------------------------------------- %%% Purpose: plugin for mysql >= 4.1 %%% Dependencies: none %%% Note: Packet fragmentation isn't implemented yet %%% --------------------------------------------------------------------- -module(ts_mysql). -vc('$Id:$ '). -author('gregoire.reboul@laposte.net'). -behavior(ts_plugin). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_mysql.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, parse/2, parse_bidi/2, dump/2, parse_config/2, decode_buffer/2, new_session/0]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session %% Returns: {ok, ack_type = parse|no_ack|local, persistent = true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true}. %% @spec decode_buffer(Buffer::binary(),Session::record(mysql)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#mysql_session{}) -> Buffer. % FIXME ? %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #mysql_session{}. parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data,State). dump(A,B) -> ts_plugin:dump(A,B). %%---------------------------------------------------------------------- %% Function: get_message/21 %% Purpose: Build a message/request , %% Args: record %% Returns: binary %%---------------------------------------------------------------------- get_message(#mysql_request{type=connect},#state_rcv{session=S}) -> Packet=list_to_binary([]), ?LOGF("Opening socket. ~p ~n",[Packet], ?DEB), {Packet,S}; get_message(#mysql_request{type=authenticate, database=Database, username=Username, passwd=Password, salt=Salt},#state_rcv{session=S}) -> Packet=add_header(make_auth(Username, Password, Database, Salt),1), ?LOGF("Auth packet: ~p (~s)~n",[Packet,Packet], ?DEB), {Packet,S}; get_message(#mysql_request{type=sql,sql=Query},#state_rcv{session=S}) -> Packet=add_header([?MYSQL_QUERY_OP, Query],0), ?LOGF("Query packet: ~p (~s)~n",[Packet,Packet], ?DEB), {Packet,S}; get_message(#mysql_request{type=close},#state_rcv{session=S}) -> Packet=add_header([?MYSQL_CLOSE_OP],0), ?LOGF("Close packet: ~p (~s)~n",[Packet,Packet], ?DEB), {Packet,S}. %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: parse the response from the server and keep information %% about the response in State#state_rcv.session %% Args: Data (binary), State (#state_rcv) %% Returns: {NewState, Options for socket (list), Close = true|false} %%---------------------------------------------------------------------- parse(closed, State) -> ?LOG("Parsing> socket closed ~n", ?WARN), {State#state_rcv{ack_done = true, datasize=0}, [], true}; parse(Data, State)-> <> = Data, case PacketSize =< size(PacketBody) of true -> ?LOG("Parsing> full packet ~n",?DEB), Request = State#state_rcv.request, Param = Request#ts_request.param, case Param#mysql_request.type of connect -> parse_greeting(PacketBody,State); authenticate -> parse_result(PacketBody,State); sql -> parse_result(PacketBody,State); close -> {State#state_rcv{ack_done = true, datasize=size(Data)},[],false} end; false -> ?LOGF("Parsing> incomplete packet: size->~p body->~p ~n",[PacketSize,size(PacketBody)], ?WARN), {State#state_rcv{ack_done = false, datasize=size(Data), acc=PacketBody},[],false} end. parse_greeting(Data, State=#state_rcv{acc = [],session=S, datasize= 0}) -> ?LOGF("Parsing greeting ~p ~n",[Data], ?DEB), Salt= get_salt(Data), NewS=S#mysql_session{salt=Salt}, {State#state_rcv{ack_done = true, datasize=size(Data), session=NewS},[],false}. parse_result(Data,State)-> case Data of <> -> case Fieldcount of 0 -> %% No Tabular data <> = Rest2, ?LOGF("OK, No Data, Row affected: ~p (~s)~n", [AffectedRows,Data], ?DEB); 255 -> <> = Rest2, ?LOGF("Error: ~p ~s ~s ~n", [Errno,SQLState, Message], ?WARN), %% FIXME: should we stop if an error occurs ? ts_mon_cache:add({ count, list_to_atom("error_mysql_"++integer_to_list(Errno))}); 254 when size(Rest2) < 9 -> ?LOGF("EOF: (~p) ~n", [Rest2], ?DEB); _ -> ?LOGF("OK, Tabular Data, Columns count: ~p (~s)~n", [Fieldcount,Data], ?DEB) end, {State#state_rcv{ack_done = true,datasize=size(Data)},[],false}; _ -> ?LOG("Bad packet ", ?ERR), ts_mon_cache:add({ count, error_mysql_badpacket}), {State#state_rcv{ack_done = true,datasize=size(Data)},[],false} end. %%---------------------------------------------------------------------- %% Function: parse_config/2 %% Purpose: parse tags in the XML config file related to the protocol %% Returns: List %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_mysql:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %% (this is used for ex. for Cookies in HTTP) %% for postgres, use this to store the auth method and salt %% Args: Subst (true|false), DynData = #dyndata, Param = #myproto_request %% Host = String %% Returns: #mysql_request %%---------------------------------------------------------------------- add_dynparams(false, {_DynVars, Session}, Param, HostData) -> add_dynparams(Session, Param, HostData); add_dynparams(true, {DynVars, Session}, Param, HostData) -> NewParam = subst(Param, DynVars), add_dynparams(Session,NewParam, HostData). add_dynparams(DynMysql, Param, _HostData) -> Param#mysql_request{salt=DynMysql#mysql_session.salt}. %%---------------------------------------------------------------------- %% Function: subst/2 %% Purpose: Replace on the fly dynamic element of the request. %% Returns: #mysql_request %%---------------------------------------------------------------------- subst(Req=#mysql_request{sql=SQL}, DynVars) -> Req#mysql_request{sql=ts_search:subst(SQL, DynVars)}. %%% -- Internal funs -------------------- add_header(Packet,SeqNum) -> BPacket=list_to_binary(Packet), <<(size(BPacket)):24/little, SeqNum:8, BPacket/binary>>. get_salt(PacketBody) -> << _Protocol:8/little, Rest/binary>> = PacketBody, {_Version, Rest2} = asciz_binary(Rest,[]), <<_TreadID:32/little, Rest3/binary>> = Rest2, {Salt, Rest4} = asciz_binary(Rest3,[]), <<_Caps:16/little, Rest5/binary>> = Rest4, <<_ServerChar:16/binary-unit:8, Rest6/binary>> = Rest5, {Salt2, _Rest7} = asciz_binary(Rest6,[]), Salt ++ Salt2. make_auth(User, "", Database, _Salt) -> Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?PROTOCOL_41 bor ?TRANSACTIONS bor ?SECURE_CONNECTION bor ?CONNECT_WITH_DB, Maxsize = ?MAX_PACKET_SIZE, UserB = list_to_binary(User), DatabaseB = list_to_binary(Database), binary_to_list(<>); make_auth(User, Password, Database, Salt) -> EncryptedPassword = encrypt_password(Password, Salt), Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?PROTOCOL_41 bor ?TRANSACTIONS bor ?SECURE_CONNECTION bor ?CONNECT_WITH_DB, Maxsize = ?MAX_PACKET_SIZE, UserB = list_to_binary(User), PasswordL = size(EncryptedPassword), DatabaseB = list_to_binary(Database), binary_to_list(<>). encrypt_password(Password, Salt) -> Stage1= case catch crypto:hash(sha,Password) of {'EXIT',_} -> crypto:start(), crypto:hash(sha,Password); Sha -> Sha end, Stage2 = crypto:hash(sha,Stage1), Res = crypto:hash_final(crypto:hash_update(crypto:hash_update(crypto:hash_init(sha), Salt), Stage2)), bxor_binary(Res, Stage1). %% @doc Find the first zero-byte in Data and add everything before it %% to Acc, as a string. %% %% @spec asciz_binary(Data::binary(), Acc::list()) -> %% {NewList::list(), Rest::binary()} asciz_binary(<<>>, Acc) -> {lists:reverse(Acc)}; asciz_binary(<<0:8, Rest/binary>>, Acc) -> {lists:reverse(Acc), Rest}; asciz_binary(<>, Acc) -> asciz_binary(Rest, [C | Acc]). dualmap(_F, [], []) -> []; dualmap(F, [E1 | R1], [E2 | R2]) -> [F(E1, E2) | dualmap(F, R1, R2)]. bxor_binary(B1, B2) -> list_to_binary(dualmap(fun (E1, E2) -> E1 bxor E2 end, binary_to_list(B1), binary_to_list(B2))). tsung-1.8.0/src/tsung/ts_mqtt.erl0000644000201100017670000003664614377756736016543 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_mqtt). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -behavior(ts_plugin). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_mqtt.hrl"). -include("mqtt.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, parse/2, dump/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). -export([ping_loop/3]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session (persistent & bidirectional) %% Returns: {ok, true|false, true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true, true}. %% @spec decode_buffer(Buffer::binary(),Session::record(jabber)) -> %% NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer, #mqtt_session{}) -> Buffer. %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #mqtt_session{}. dump(A, B) -> ts_plugin:dump(A,B). %%---------------------------------------------------------------------- %% Function: get_message/1 %% Purpose: Build a message/request , %% Args: record %% Returns: binary %%---------------------------------------------------------------------- get_message(Req0 = #mqtt_request{type = connect, client_id = undefined}, StateRcv) -> ClientId = ["tsung-", ts_utils:randombinstr(10)], Req1 = Req0#mqtt_request{client_id = ClientId}, get_message(Req1, StateRcv); get_message(#mqtt_request{type = connect, clean_start = CleanStart, keepalive = KeepAlive, will_topic = WillTopic, will_qos = WillQos, will_msg = WillMsg, will_retain = WillRetain, username = UserName, password = Password, client_id = ClientId}, #state_rcv{session = MqttSession}) -> PublishOptions = mqtt_frame:set_publish_options([{qos, WillQos}, {retain, WillRetain}]), Will = #will{topic = WillTopic, message = WillMsg, publish_options = PublishOptions}, Options = mqtt_frame:set_connect_options([{client_id, ClientId}, {clean_start, CleanStart}, {keepalive, KeepAlive}, {username, UserName}, {password, Password}, Will]), Message = #mqtt{type = ?CONNECT, arg = Options}, {mqtt_frame:encode(Message), MqttSession#mqtt_session{wait = ?CONNACK, keepalive = KeepAlive}}; get_message(#mqtt_request{type = disconnect}, #state_rcv{session = MqttSession}) -> PingPid = MqttSession#mqtt_session.ping_pid, PingPid ! stop, Message = #mqtt{type = ?DISCONNECT}, ts_mon_cache:add({count, mqtt_disconnected}), {mqtt_frame:encode(Message), MqttSession#mqtt_session{wait = none, status = disconnect}}; get_message(#mqtt_request{type = publish, topic = Topic, qos = Qos, retained = Retained, payload = Payload, stamped = Stamped}, #state_rcv{session = MqttSession = #mqtt_session{curr_id = Id}}) -> NewMqttSession = case Qos of 0 -> MqttSession; _ -> MqttSession#mqtt_session{curr_id = Id + 1} end, NewPayload = case Stamped of true -> generate_stamp() ++ Payload; _ -> Payload end, MsgId = NewMqttSession#mqtt_session.curr_id, Message = #mqtt{id = MsgId, type = ?PUBLISH, qos = Qos, retain = Retained, arg = {Topic, NewPayload}}, Wait = case Qos of 1 -> ?PUBACK; _ -> none end, ts_mon_cache:add({count, mqtt_published}), {mqtt_frame:encode(Message), NewMqttSession#mqtt_session{wait = Wait}}; get_message(#mqtt_request{type = subscribe, topic = Topic, qos = Qos}, #state_rcv{session = MqttSession = #mqtt_session{curr_id = Id}}) -> NewMqttSession = MqttSession#mqtt_session{curr_id = Id + 1}, Arg = [#sub{topic = Topic, qos = Qos}], MsgId = NewMqttSession#mqtt_session.curr_id, Message = #mqtt{id = MsgId, type = ?SUBSCRIBE, arg = Arg, qos = 1}, {mqtt_frame:encode(Message), NewMqttSession#mqtt_session{wait = ?SUBACK}}; get_message(#mqtt_request{type = unsubscribe, topic = Topic}, #state_rcv{session = MqttSession = #mqtt_session{curr_id = Id}}) -> NewMqttSession = MqttSession#mqtt_session{curr_id = Id + 1}, Arg = [#sub{topic = Topic}], MsgId = NewMqttSession#mqtt_session.curr_id, Message = #mqtt{id = MsgId, type = ?UNSUBSCRIBE, arg = Arg}, {mqtt_frame:encode(Message),NewMqttSession#mqtt_session{wait = ?UNSUBACK}}. %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: parse the response from the server and keep information %% about the response in State#state_rcv.session %% Args: Data (binary), State (#state_rcv) %% Returns: {NewState, Options for socket (list), Close = true|false} %%---------------------------------------------------------------------- parse(closed, State) -> {State#state_rcv{ack_done = true, datasize=0}, [], true}; %% new response, compute data size (for stats) parse(Data, State=#state_rcv{acc = [], datasize= 0}) -> parse(Data, State#state_rcv{datasize= size(Data)}); %% normal mqtt message parse(Data, State=#state_rcv{acc = [], session = MqttSession, socket = Socket}) -> Wait = MqttSession#mqtt_session.wait, AckBuf = MqttSession#mqtt_session.ack_buf, case mqtt_frame:decode(Data) of {_MqttMsg = #mqtt{type = Wait}, Left} -> ?DebugF("receive mqtt_msg: ~p ~p~n", [mqtt_frame:command_for_type(Wait), _MqttMsg]), NewLeft = case Wait of ?SUBACK -> <<>>; _ -> Left end, case Wait of ?CONNACK -> ts_mon_cache:add({count, mqtt_connected}); ?PUBACK -> ts_mon_cache:add({count, mqtt_server_pubacked}); ?SUBACK -> case {AckBuf, Left} of {<<>>, <<>>} -> ok; _ -> self() ! {gen_ts_transport, Socket, Left} end; _ -> ok end, NewMqttSession = case Wait of ?CONNACK -> Proto = State#state_rcv.protocol, KeepAlive = MqttSession#mqtt_session.keepalive, PingPid = create_ping_proc(Proto, Socket, KeepAlive), MqttSession#mqtt_session{ping_pid = PingPid}; _ -> MqttSession end, {State#state_rcv{ack_done = true, acc = NewLeft, session = NewMqttSession}, [], false}; {_MqttMsg = #mqtt{id = MessageId, type = Type, qos = Qos}, Left} -> ?DebugF("receive mqtt_msg, expecting: ~p, actual: ~p ~p~n", [mqtt_frame:command_for_type(Wait), mqtt_frame:command_for_type(Type), _MqttMsg]), NewMqttSession = case {Wait, Type, Qos} of {?SUBACK, ?PUBLISH, 1} -> Message = #mqtt{type = ?PUBACK, arg = MessageId}, EncodedData = mqtt_frame:encode(Message), ts_mon_cache:add({count, mqtt_server_published}), NewAckBuf = <>, MqttSession#mqtt_session{ack_buf = NewAckBuf}; _ -> MqttSession end, {State#state_rcv{ack_done = false, acc = Left, session = NewMqttSession}, [], false}; more -> ?DebugF("incomplete mqtt frame: ~p~n", [Data]), {State#state_rcv{acc = Data}, [], false} end; %% more data, add this to accumulator and parse, update datasize parse(Data, State=#state_rcv{acc = Acc, datasize = DataSize}) -> NewSize= DataSize + size(Data), parse(<< Acc/binary, Data/binary >>, State#state_rcv{acc = [], datasize = NewSize}). parse_bidi(<<>>, State=#state_rcv{acc = [], session = MqttSession}) -> AckBuf = MqttSession#mqtt_session.ack_buf, Ack = case AckBuf of <<>> -> nodata; _ -> AckBuf end, NewMqttSession = MqttSession#mqtt_session{ack_buf = <<>>}, ?DebugF("ack buf: ~p~n", [AckBuf]), {Ack, State#state_rcv{session = NewMqttSession}, think}; parse_bidi(Data, State=#state_rcv{acc = [], session = MqttSession}) -> AckBuf = MqttSession#mqtt_session.ack_buf, case mqtt_frame:decode(Data) of {_MqttMsg = #mqtt{type = ?PUBLISH, qos = Qos, id = MessageId, arg = {_, Payload}}, Left} -> ?DebugF("receive bidi mqtt_msg: ~p ~p~n", [mqtt_frame:command_for_type(?PUBLISH), _MqttMsg]), ts_mon_cache:add({count, mqtt_server_published}), ts_mon_cache:add({count, mqtt_pubacked}), parse_stamp(Payload), Ack = case Qos of 1 -> Message = #mqtt{type = ?PUBACK, arg = MessageId}, mqtt_frame:encode(Message); _ -> <<>> end, NewAckBuf = <>, NewMqttSession = MqttSession#mqtt_session{ack_buf = NewAckBuf}, parse_bidi(Left, State#state_rcv{session = NewMqttSession}); {_MqttMsg = #mqtt{type = _Type}, Left} -> ?DebugF("receive bidi mqtt_msg: ~p ~p~n", [mqtt_frame:command_for_type(_Type), _MqttMsg]), parse_bidi(Left, State); more -> {nodata, State#state_rcv{acc = Data},think} end; parse_bidi(Data, State=#state_rcv{acc = Acc, datasize = DataSize}) -> NewSize = DataSize + size(Data), ?DebugF("parse mqtt bidi data: ~p ~p~n", [Data, Acc]), parse_bidi(<>, State#state_rcv{acc = [], datasize = NewSize}). %%---------------------------------------------------------------------- %% Function: parse_config/2 %% Purpose: parse tags in the XML config file related to the protocol %% Returns: List %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_mqtt:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: we dont actually do anything %% Returns: #websocket_request %%---------------------------------------------------------------------- add_dynparams(true, {DynVars, _S}, Param = #mqtt_request{type = connect, clean_start = CleanStart, keepalive = KeepAlive, will_topic = WillTopic, will_qos = WillQos, will_msg = WillMsg, will_retain = WillRetain, username = UserName, password = Password, client_id = ClientId}, _HostData) -> NewUserName = ts_search:subst(UserName, DynVars), NewPassword = ts_search:subst(Password, DynVars), NewWillTopic = ts_search:subst(WillTopic, DynVars), NewClientId = ts_search:subst(ClientId, DynVars), Param#mqtt_request{ type = connect, clean_start = CleanStart, keepalive = KeepAlive, will_topic = NewWillTopic, will_qos = WillQos, will_msg = WillMsg, will_retain = WillRetain, username = NewUserName, password = NewPassword, client_id = NewClientId}; add_dynparams(true, {DynVars, _S}, Param = #mqtt_request{type = publish, topic = Topic, payload = Payload}, _HostData) -> NewTopic = ts_search:subst(Topic, DynVars), NewPayload = ts_search:subst(Payload, DynVars), Param#mqtt_request{topic = NewTopic, payload = NewPayload}; add_dynparams(true, {DynVars, _S}, Param = #mqtt_request{type = subscribe, topic = Topic}, _HostData) -> NewTopic = ts_search:subst(Topic, DynVars), Param#mqtt_request{topic = NewTopic}; add_dynparams(true, {DynVars, _S}, Param = #mqtt_request{type = unsubscribe, topic = Topic}, _HostData) -> NewTopic = ts_search:subst(Topic, DynVars), Param#mqtt_request{topic = NewTopic}; add_dynparams(_Bool, _DynData, Param, _HostData) -> Param#mqtt_request{}. %%%=================================================================== %%% Internal functions %%%=================================================================== create_ping_proc(Proto, Socket, KeepAlive) -> PingPid = proc_lib:spawn_link(?MODULE, ping_loop, [Proto, Socket, KeepAlive]), erlang:send_after(KeepAlive * 1000, PingPid, ping), PingPid. ping_loop(Proto, Socket, KeepAlive) -> receive ping -> try Message = #mqtt{type = ?PINGREQ}, PingFrame = mqtt_frame:encode(Message), Proto:send(Socket, PingFrame, []) catch Error -> ?LOGF("Error sending mqtt pingreq: ~p~n",[Error], ?ERR) end, erlang:send_after(KeepAlive * 1000, self(), ping), ping_loop(Proto, Socket, KeepAlive); stop -> ok end. generate_stamp() -> {Mega, Secs, Micro} = ?TIMESTAMP, TS = integer_to_list(Mega) ++ ";" ++ integer_to_list(Secs) ++ ";" ++ integer_to_list(Micro), "@@@" ++ integer_to_list(erlang:phash2(node())) ++ "," ++ TS ++ "@@@". parse_stamp(Payload) -> case re:run(Payload, "@@@([^@]+)@@@", [{capture, all_but_first, list}]) of {match, [NodeStamp]} -> [NodeS, StampS] = string:tokens(NodeStamp, ","), case integer_to_list(erlang:phash2(node())) of NodeS -> [MegaS, SecsS, MicroS] = string:tokens(StampS, ";"), Mega = list_to_integer(MegaS), Secs = list_to_integer(SecsS), Micro = list_to_integer(MicroS), Latency = timer:now_diff(?TIMESTAMP, {Mega, Secs, Micro}), ts_mon_cache:add({ sample, mqtt_forward_latency, Latency / 1000}); _ -> ignore end; nomatch -> nomatch end. tsung-1.8.0/src/tsung/ts_mon_cache.erl0000644000201100017670000002221714377756736017457 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_session_cache.erl %%% Author : Nicolas Niclausse %%% Description : cache sessions request from ts_config_server %%% %%% Created : 2 Dec 2003 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_mon_cache). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% External exports -export([start/0, add/1, add_match/2, dump/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, { stats=[], % cache stats msgs transactions=[], % cache transaction stats msgs pages=[], % cache pages stats msgs requests=[], % cache requests stats msgs connections=[], % cache connect stats msgs match=[], % cache match logs protocol=[], % cache dump=protocol data sum % cache sum stats msgs }). -include("ts_config.hrl"). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link/0 %% Description: Starts the server %%-------------------------------------------------------------------- start() -> ?LOG("Starting~n",?INFO), gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% Function: add/1 %% Description: Add stats data. Will be accumulated sent periodically %% to ts_mon %%-------------------------------------------------------------------- add(Data) -> gen_server:cast(?MODULE, {add, Data}). %% @spec add_match(Data::list(),{UserId::integer(),SessionId::integer(),RequestId::integer(), %% TimeStamp::tuple(),Transactions::list(),Name::atom()}) -> ok; %% (Data::list(),{UserId::integer(),SessionId::integer(),RequestId::integer(), %% TimeStamp::tuple(),Bin::list(),Transactions::list(),Name::atom()}) -> ok. add_match(Data,{UserId,SessionId,RequestId,Tr,Name}) -> add_match(Data,{UserId,SessionId,RequestId,[],Tr,Name}); add_match(Data,{UserId,SessionId,RequestId,Bin,Tr,Name}) -> TimeStamp=?TIMESTAMP, add_match(Data,{UserId,SessionId,RequestId,TimeStamp,Bin,Tr,Name}); add_match(Data=[Head|_],{UserId,SessionId,RequestId,TimeStamp,Bin,Tr,Name}) -> put(last_match,Head), gen_server:cast(?MODULE, {add_match, Data, {UserId,SessionId,RequestId,TimeStamp,Bin,Tr,Name}}). %% @spec dump({Type, Who, What}) -> ok @end dump({none, _, _}) -> skip; dump({_Type, Who, What}) -> gen_server:cast(?MODULE, {dump, Who, ?TIMESTAMP, What}). %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init([]) -> erlang:start_timer(?CACHE_DUMP_STATS_INTERVAL, self(), dump_stats ), {ok, #state{sum=dict:new()}}. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast({add, Data}, State) when is_list(Data) -> LastState = lists:foldl(fun(NewData,NewState)-> update_stats(NewData,NewState) end, State, Data), {noreply, LastState }; handle_cast({add, Data}, State) when is_tuple(Data) -> {noreply,update_stats(Data, State)}; handle_cast({add_match, Data=[First|_Tail],{UserId,SessionId,RequestId,TimeStamp,Bin,Tr,Name}}, State=#state{stats=List, match=MatchList})-> NewMatchList=lists:append([{UserId,SessionId,RequestId,TimeStamp,First, Bin, Tr,Name}], MatchList), {noreply, State#state{stats = lists:append(Data, List), match = NewMatchList}}; handle_cast({dump, Who, When, What}, State=#state{protocol=Cache}) -> Log = io_lib:format("~w;~w;~s~n",[ts_utils:time2sec_hires(When),Who,What]), {noreply, State#state{protocol=[Log|Cache]}}; handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info({timeout, _Ref, dump_stats}, State =#state{protocol=ProtocolData, stats= Stats, match=MatchList}) -> Fun = fun(Key,Val, Acc) -> [{sum,Key,Val}| Acc] end, NewStats=dict:fold(Fun, Stats, State#state.sum), ts_stats_mon:add(NewStats), ts_stats_mon:add(State#state.requests,request), ts_stats_mon:add(State#state.connections,connect), ts_stats_mon:add(State#state.transactions,transaction), ts_stats_mon:add(State#state.pages,page), ts_mon:dump({cached, list_to_binary(lists:reverse(ProtocolData))}), ts_match_logger:add(MatchList), erlang:start_timer(?CACHE_DUMP_STATS_INTERVAL, self(), dump_stats ), {noreply, State#state{protocol=[],stats=[],match=[],pages=[],requests=[],transactions=[],connections=[],sum=dict:new()}}; handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(Reason, _State) -> ?LOGF("Die ! (~p)~n",[Reason],?ERR), ok. %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. update_stats({sample, request, Val}, State=#state{requests=L}) -> State#state{requests=lists:append([Val],L)}; update_stats({sample, page, Val}, State=#state{pages=L}) -> State#state{pages=lists:append([Val],L)}; update_stats({sample, connect, Val}, State=#state{connections=L}) -> State#state{connections=lists:append([Val],L)}; update_stats(S={sample, _Type, _}, State=#state{transactions=L}) -> State#state{transactions=lists:append([S],L)}; update_stats({sum, Type, Val}, State=#state{sum=Sum}) -> NewSum=dict:update_counter(Type,Val,Sum), State#state{sum=NewSum}; update_stats({count, Type}, State=#state{sum=Sum}) -> NewSum=dict:update_counter(Type,1,Sum), State#state{sum=NewSum}; update_stats(Data, State=#state{stats=L}) when is_tuple(Data)-> State#state{stats=lists:append([Data],L)}. tsung-1.8.0/src/tsung/ts_local_mon.erl0000644000201100017670000001500214377756736017500 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% File : ts_local_mon.erl %%% Author : Nicolas Niclausse %%% Description : local logger for protocol_local option %%% %%% Created : 5 May 2014 by Nicolas Niclausse %%%------------------------------------------------------------------- -module(ts_local_mon). -behaviour(gen_server). %%-------------------------------------------------------------------- %% Include files %%-------------------------------------------------------------------- %%-------------------------------------------------------------------- %% External exports -export([start/0, dump/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, { dump_iodev % ioDev for local dump file }). -define(DUMP_STATS_INTERVAL, 500). % in milliseconds -include("ts_macros.hrl"). %%==================================================================== %% External functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link/0 %% Description: Starts the server %%-------------------------------------------------------------------- start() -> ?LOG("Starting~n",?INFO), gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% Function: add/1 %% Description: Add stats data. Will be accumulated sent periodically %% to ts_mon %%-------------------------------------------------------------------- dump({_Type, Who, What}) -> gen_server:cast(?MODULE, {dump, Who, ?TIMESTAMP, What}). %%==================================================================== %% Server functions %%==================================================================== %%-------------------------------------------------------------------- %% Function: init/1 %% Description: Initiates the server %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%-------------------------------------------------------------------- init([]) -> %% erlang:start_timer(?DUMP_STATS_INTERVAL, self(), dump_stats ), Id = integer_to_list(ts_utils:get_node_id()), LogFileEnc = ts_config_server:decode_filename(?config(log_file)), FileName = filename:join(LogFileEnc, "tsung-"++Id ++ ".dump"), LogDir = filename:dirname(FileName), case ts_utils:make_dir_raw(LogDir) of ok -> case file:open(FileName,[write,raw, delayed_write]) of {ok, IODev} -> {ok, #state{dump_iodev=IODev}}; {error, Reason} -> ?LOGF("Can't open dump file ~p on node ~p: ~p",[FileName, node(), Reason],?ERR), {ok,#state{}} end; {error, Reason} -> ?LOGF("Can't create directory ~p in node ~p: protocol_local won't work. Reason: ~p",[LogDir, node(), Reason],?WARN), {ok,#state{}} end. %%-------------------------------------------------------------------- %% Function: handle_call/3 %% Description: Handling call messages %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_call(_Request, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast/2 %% Description: Handling cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_cast(_, State=#state{dump_iodev=undefined}) -> {noreply, State}; handle_cast({dump, Who, When, What}, State=#state{dump_iodev=IODev}) -> Data = io_lib:format("~w;~w;~s~n",[ts_utils:time2sec_hires(When),Who,What]), file:write(IODev,Data), {noreply, State}; handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info/2 %% Description: Handling all non call/cast messages %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate/2 %% Description: Shutdown the server %% Returns: any (ignored by gen_server) %%-------------------------------------------------------------------- terminate(_, #state{dump_iodev=undefined}) -> ok; terminate(_Reason, #state{dump_iodev=IODev}) -> file:close(IODev). %%-------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. tsung-1.8.0/src/tsung/ts_ldap.erl0000644000201100017670000003012414377756736016457 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% File : ts_ldap.erl %%% Author : Pablo Polvorin %%% Purpose : LDAP plugin -module(ts_ldap). -behavior(ts_plugin). -export([add_dynparams/4, get_message/2, session_defaults/0, dump/2, parse/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0 ]). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_ldap.hrl"). -include("ELDAPv3.hrl"). %%---------------------------------------------------------------- %%-----Configuration parsing %%---------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_ldap:parse_config(Element,Conf). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session %% Returns: {ok, persistent = true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true}. %% @spec decode_buffer(Buffer::binary(),Session::record(ldap)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,_) -> Buffer. %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> ts_ldap_common:empty_packet_state(). %%FIXME: this won't be necessary when the SSL module support the asn1 %% packet type. At this moment we are parsing the packet by %% ourselves, even over plain gen_tcp sockets, which can %% recognize asn1... dump(A,B)-> ts_plugin:dump(A,B). parse_bidi(A, B) -> ts_plugin:parse_bidi(A,B). %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: parse the response from the server and keep information %% about the response in State#state_rcv.session %% Args: Data (binary), State (#state_rcv) %% Returns: {NewState, Options for socket (list), Close = true|false} %%---------------------------------------------------------------------- parse(closed, State) -> {State#state_rcv{ack_done = true, datasize=0}, [], true}; %% Shortcut, when using ssl i'm getting lots <<>> data. Also, next %% clause is an infinite loop if data is <<>> parse(<<>>,State) -> {State,[],false}; %% new response, compute data size (for stats) parse(Data, State=#state_rcv{acc = [], datasize= 0}) -> parse(Data, State#state_rcv{datasize= size(Data)}); parse(Data, State=#state_rcv{acc = [], session=Session,datasize=PrevSize}) -> St = ts_ldap_common:push(Data,Session), parse_packets(State#state_rcv{session=St,datasize =PrevSize + size(Data) },St). %% Can read more than one entire asn1 packet from the network. Read %% packets until either there are no more packets available in the %% buffer (ack_done=false), or the ack_done flag was set true by the %% appropriate parse_ldap_response parse_packets(State,Asn1St) -> case ts_ldap_common:get_packet(Asn1St) of {none,NewAsn1St} -> {State#state_rcv{ack_done=false,session=NewAsn1St},[],false}; {packet,Packet,NewAsn1St} -> {ok,Resp} = 'ELDAPv3':decode('LDAPMessage', Packet), parse_packet(Resp,State#state_rcv{session = NewAsn1St}) end. parse_packet(Resp,State) -> R = parse_ldap_response(Resp,State), {St,_Opts,_Close} = R, if St#state_rcv.ack_done == true -> R; St#state_rcv.ack_done == false -> parse_packets(St,St#state_rcv.session) end. %%TODO: see if its useful to count how many response records we get for each search. parse_ldap_response( #'LDAPMessage'{protocolOp = {bindResponse,Result}},State)-> case Result#'BindResponse'.resultCode of success -> ?Debug("Bind successful~n"), ts_mon_cache:add({ count, ldap_bind_ok}), {State#state_rcv{ack_done=true},[],false}; _Error -> ts_mon_cache:add({ count, ldap_bind_error}), %FIXME: retry,fail,etc. should be configurable ?LOG("Bind fail~n",?INFO), {State#state_rcv{ack_done=true},[],true} end; parse_ldap_response( #'LDAPMessage'{protocolOp = {'searchResDone',_R}},State) -> ?DebugF("LDAP Search response Done ~p~n",[_R]), {State#state_rcv{ack_done=true},[],false}; %%Response done, mark as acknowledged parse_ldap_response( #'LDAPMessage'{protocolOp = {'searchResEntry',R}},State) -> NewState = acumulate_result(R,State), ?DebugF("LDAP search response Entry ~p~n",[R]), {NewState#state_rcv{ack_done=false},[],false}; parse_ldap_response(#'LDAPMessage'{protocolOp = {'searchResRef',_R}},State) -> ?DebugF("LDAP search response Ref ~p~n",[_R]), {State#state_rcv{ack_done=false},[],false}; %% When get a positive response to a startTLS command, immediately start ssl over that socket. parse_ldap_response(#'LDAPMessage'{protocolOp = {'extendedResp',ExtResponse }},State) -> case ExtResponse#'ExtendedResponse'.resultCode of success -> #ts_request{param = LDAPRequest} = State#state_rcv.request, %%Warnning: this won't work unless using a really recent OTP {ok,Ssl_socket} = ssl:connect(State#state_rcv.socket,[{cacertfile,LDAPRequest#ldap_request.cacertfile}, {certfile,LDAPRequest#ldap_request.certfile}, {keyfile,LDAPRequest#ldap_request.keyfile} ]), {State#state_rcv{socket=Ssl_socket,protocol=ssl,ack_done=true},[],false}; _Error -> ts_mon_cache:add({ count, ldap_starttls_error}), ?LOG("StartTLS fail",?INFO), {State#state_rcv{ack_done=true},[],false} end; parse_ldap_response(#'LDAPMessage'{protocolOp = {'addResponse',Result}},State) -> case Result#'LDAPResult'.resultCode of success -> {State#state_rcv{ack_done=true},[],false}; _Error -> ts_mon_cache:add({ count, ldap_add_error}), ?LOG("Add fail",?INFO), {State#state_rcv{ack_done=true},[],true} end; parse_ldap_response(#'LDAPMessage'{protocolOp = {'modifyResponse',Result}},State) -> case Result#'LDAPResult'.resultCode of success -> {State#state_rcv{ack_done=true},[],false}; _Error -> ts_mon_cache:add({ count, ldap_modify_error}), ?LOG("Modify fail",?INFO), {State#state_rcv{ack_done=true},[],true} end; parse_ldap_response(Resp,State) -> ?LOGF("Got unexpected response: ~p~n",[Resp],?INFO), ts_mon_cache:add({ count, ldap_unexpected_msg_resp}), {State#state_rcv{ack_done=true},[],false}. acumulate_result(R,State = #state_rcv{request = #ts_request{param=#ldap_request{result_var = ResultVar}}, dynvars = DynVars}) -> case ResultVar of none -> State; {ok,VarName} -> State#state_rcv{dynvars=accumulate_dyndata(R,VarName,DynVars)} end. accumulate_dyndata(R,VarName,DynVars) when is_list(DynVars)-> Prev = proplists:get_value(VarName,DynVars,[]), NewDynVars = lists:keystore(VarName,1,DynVars,{VarName,[R|Prev]}), NewDynVars; accumulate_dyndata(R,VarName,_DynVars) -> [{VarName,[R]}]. %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %% Args: Subst (true|false), DynData = #dyndata, Param = #myproto_request %% Host = String %% Returns: #ldap_request %% %%---------------------------------------------------------------------- add_dynparams(false, _DynData, Param, _HostData) -> Param; %% Bind message. Substitution on user and password. add_dynparams(true, {DynVars, _Session}, Param = #ldap_request{type=bind,user=User,password=Password}, _HostData) -> Param#ldap_request{user=ts_search:subst(User,DynVars),password=ts_search:subst(Password,DynVars)}; %% Search message. Only perform substitutions on the filter of the search requests. %% The filter text was already parsed into a tree-like struct, substitution %% is performed in the "leaf" of this tree. add_dynparams(true, {DynVars, _Session}, Param = #ldap_request{type=search, filter = Filter}, _HostData) -> Param#ldap_request{filter = subs_filter(Filter,DynVars)}; %% Add message. Substitution on DN and attrs values. add_dynparams(true,{DynVars, _Session},Param = #ldap_request{type=add,dn=DN,attrs=Attrs},_HostData) -> Param#ldap_request{dn=ts_search:subst(DN,DynVars), attrs=subs_attrs(Attrs,DynVars)}; %% Modification message. Substitution on DN and attrs values. add_dynparams(true,{DynVars, _Session},Param = #ldap_request{type=modify,dn=DN,modifications=Modifications},_HostData) -> SubsModifications = [{Operation,AttrType,[ts_search:subst(Value,DynVars) || Value <- Values]} || {Operation,AttrType,Values}<- Modifications ], Param#ldap_request{dn=ts_search:subst(DN,DynVars), attrs=SubsModifications}. subs_filter({Rel,Filters},DynVars) when (Rel == 'and') or (Rel == 'or') -> {Rel,lists:map(fun(F)-> subs_filter(F,DynVars) end,Filters)}; subs_filter({'not',Filter},DynVars) -> {'not',subs_filter(Filter,DynVars)}; subs_filter({BinRel,Attr,Val},DynVars) when (BinRel == 'aprox') or (BinRel == 'get') or (BinRel == 'let') or (BinRel=='eq')-> {BinRel,Attr,ts_search:subst(Val,DynVars)}; subs_filter({substring,Attr,Substrings},DynVars) -> {substring,Attr,lists:map(fun({Pos,Val}) -> {Pos,ts_search:subst(Val,DynVars)} end, Substrings)}. subs_attrs(Attrs,DynVars) -> [{Attr,[ts_search:subst(Value,DynVars) || Value <- Values]} || {Attr,Values}<-Attrs ]. %%---------------------------------------------------------------- %%-----Messages %%---------------------------------------------------------------- get_message(Req,#state_rcv{session=S}) -> {get_message2(Req),S}. get_message2(#ldap_request{type=bind,user=User,password=Password}) -> X = ts_ldap_common:bind_msg(ts_msg_server:get_id(),User,Password), iolist_to_binary(X); %% TODO: we really need to consult the central msg_server to find a session-specific id?, any reason to prevent %% the same id to be used in different sessions? get_message2(#ldap_request{type=search,base=Base,scope=Scope,filter=Filter,attributes=Attributes}) -> EncodedFilter = ts_ldap_common:encode_filter(Filter), X = ts_ldap_common:search_msg(ts_msg_server:get_id(),Base,Scope,EncodedFilter,Attributes), iolist_to_binary(X); get_message2(#ldap_request{type=start_tls}) -> X = ts_ldap_common:start_tls_msg(ts_msg_server:get_id()), iolist_to_binary(X); get_message2(#ldap_request{type=unbind}) -> iolist_to_binary(ts_ldap_common:unbind_msg(ts_msg_server:get_id())); get_message2(#ldap_request{type=add,dn=DN,attrs=Attrs}) -> iolist_to_binary(ts_ldap_common:add_msg(ts_msg_server:get_id(),DN,Attrs)); get_message2(#ldap_request{type=modify,dn=DN,modifications=Modifications}) -> iolist_to_binary(ts_ldap_common:modify_msg(ts_msg_server:get_id(),DN,Modifications)). tsung-1.8.0/src/tsung/ts_ldap_common.erl0000644000201100017670000001616014377756736020033 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% File : ts_ldap_common.erl %%% Author : Pablo Polvorin %%% Purpose : LDAP plugin -module(ts_ldap_common). -export([encode_filter/1, bind_msg/3, unbind_msg/1, search_msg/5, start_tls_msg/1, add_msg/3, modify_msg/3 ]). -export([push/2,get_packet/1,empty_packet_state/0]). -define(LDAP_VERSION, 3). -define(START_TLS_OID,"1.3.6.1.4.1.1466.20037"). -define(MAX_HEADER, 8). %% mm... -include("ELDAPv3.hrl"). encode_filter({'and',L}) -> {'and',lists:map(fun encode_filter/1,L)}; encode_filter({'or',L}) -> {'or',lists:map(fun encode_filter/1,L)}; encode_filter({'not',I}) -> {'not',encode_filter(I)}; encode_filter(I = {'present',_}) -> I; encode_filter({'substring',Attr,Subs}) -> eldap:substrings(Attr,Subs); encode_filter({eq,Attr,Value}) -> eldap:equalityMatch(Attr,Value); encode_filter({'let',Attr,Value}) -> eldap:lessOrEqual(Attr,Value); encode_filter({get,Attr,Value}) -> eldap:greaterOrEqual(Attr,Value); encode_filter({aproxq,Attr,Value}) -> eldap:approxMatch(Attr,Value). bind_msg(Id,User,Password) -> Req = {bindRequest,#'BindRequest'{version=?LDAP_VERSION, name=User, authentication = {simple, Password}}}, Message = #'LDAPMessage'{messageID = Id, protocolOp = Req}, {ok,Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), Bytes. search_msg(Id,Base,Scope,Filter,Attributes) -> Req = #'SearchRequest'{baseObject = Base, scope = Scope, derefAliases = neverDerefAliases, sizeLimit = 0, % no size limit timeLimit = 0, typesOnly = false, filter = Filter, attributes = Attributes}, Message = #'LDAPMessage'{messageID = Id, protocolOp = {searchRequest,Req}}, {ok,Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), Bytes. start_tls_msg(Id) -> Req = #'ExtendedRequest'{requestName = ?START_TLS_OID}, Message = #'LDAPMessage'{messageID = Id, protocolOp = {extendedReq,Req}}, {ok,Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), Bytes. unbind_msg(Id) -> Message = #'LDAPMessage'{messageID = Id, protocolOp = {unbindRequest,[]}}, {ok,Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), Bytes. add_msg(Id,DN,Attrs) -> Req = #'AddRequest'{entry = DN, attributes = [ {'AddRequest_attributes',Type, Values} || {Type,Values} <- Attrs]}, Message = #'LDAPMessage'{messageID = Id, protocolOp = {addRequest,Req}}, {ok,Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), Bytes. modify_msg(Id,DN,Modifications) -> Mods = [ #'ModifyRequest_modification_SEQOF'{ operation = Operation, modification = #'AttributeTypeAndValues'{type=Type,vals=Values}} || {Operation,Type,Values} <- Modifications], Req = #'ModifyRequest'{object = DN, modification = Mods}, Message = #'LDAPMessage'{messageID = Id, protocolOp = {modifyRequest,Req}}, {ok,Bytes} = 'ELDAPv3':encode('LDAPMessage', Message), Bytes. %% ------------------------------------------- %% asn1 packet buffering and delimiting %% %% Temporary fix until the new ssl module incorporate appropriate %% support for asn1 packets. %% ------------------------------------------- -record(asn1_packet_state, { length = undefined, buffer = <<>> }). empty_packet_state() -> #asn1_packet_state{}. push(<<>>,S) -> S; push(Data,S =#asn1_packet_state{buffer = B}) -> S#asn1_packet_state{buffer = <>}. get_packet(S = #asn1_packet_state{buffer= <<>>}) -> {none,S}; get_packet(S = #asn1_packet_state{length=undefined,buffer=Buffer}) -> case packet_length(Buffer) of {ok,Length} -> extract_packet(S#asn1_packet_state{length=Length}); not_enough_data -> {none,S} end; get_packet(S) -> extract_packet(S). extract_packet(#asn1_packet_state{length=N,buffer=Buffer}) when (size(Buffer) >= N) -> <> = Buffer, {packet,Packet,#asn1_packet_state{length=undefined,buffer=Rest}}; extract_packet(S) when is_record(S,asn1_packet_state) -> {none,S}. packet_length(Buffer) -> try compat_asn1rt_ber_bin_decode_tag_and_length(Buffer) of {_Tag, Len,_Rest,RemovedBytes} -> {ok,Len+RemovedBytes} catch _Type:_Error -> case size(Buffer) > ?MAX_HEADER of true -> throw({invalid_packet,Buffer}); false -> not_enough_data end end. %% ------------------------------------------- %% old asn1rt_ber_bin compat functions %% %% ------------------------------------------- %% compat_asn1rt_ber_bin_decode_tag_and_length(Buffer) -> {Tag, Buffer2, RemBytesTag} = compat_asn1rt_ber_bin_decode_tag(Buffer), {{Len, Buffer3}, RemBytesLen} = compat_asn1rt_ber_bin_decode_length(Buffer2), {Tag, Len, Buffer3, RemBytesTag+RemBytesLen}. %% multiple octet tag compat_asn1rt_ber_bin_decode_tag(<>) -> {TagNo, Buffer1, RemovedBytes} = compat_asn1rt_ber_bin_decode_tag(Buffer, 0, 1), {{(Class bsl 6), (Form bsl 5), TagNo}, Buffer1, RemovedBytes}; %% single tag (< 31 tags) compat_asn1rt_ber_bin_decode_tag(<>) -> {{(Class bsl 6), (Form bsl 5), TagNo}, Buffer, 1}. %% last partial tag compat_asn1rt_ber_bin_decode_tag(<<0:1,PartialTag:7, Buffer/binary>>, TagAck, RemovedBytes) -> TagNo = (TagAck bsl 7) bor PartialTag, %%<> = <>, {TagNo, Buffer, RemovedBytes+1}; % more tags compat_asn1rt_ber_bin_decode_tag(<<_:1,PartialTag:7, Buffer/binary>>, TagAck, RemovedBytes) -> TagAck1 = (TagAck bsl 7) bor PartialTag, %%<> = <>, compat_asn1rt_ber_bin_decode_tag(Buffer, TagAck1, RemovedBytes+1). compat_asn1rt_ber_bin_decode_length(<<1:1,0:7,T/binary>>) -> {{indefinite, T}, 1}; compat_asn1rt_ber_bin_decode_length(<<0:1,Length:7,T/binary>>) -> {{Length,T},1}; compat_asn1rt_ber_bin_decode_length(<<1:1,LL:7,T/binary>>) -> <> = T, {{Length,Rest}, LL+1}. tsung-1.8.0/src/tsung/ts_launcher_static.erl0000644000201100017670000002166014377756736020714 0ustar nniclausdream%%% Copyright (C) 2009 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_launcher_static). -created('Date: 2009/03/10 19:09:57 nniclausse '). -vc('$Id: ts_launcher.erl 968 2008-12-16 12:51:28Z nniclausse $ '). -author('nicolas.niclausse@niclux.org'). -include("ts_profile.hrl"). -include("ts_config.hrl"). -behaviour(gen_fsm). %% a primitive gen_fsm with two state: launcher and wait %% External exports -export([start/0, launch/1, stop/1]). %% gen_fsm callbacks -export([init/1, launcher/2, wait/2, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). -record(state, { myhostname, users }). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: start/0 %%-------------------------------------------------------------------- start() -> ?LOG("starting ~n", ?INFO), gen_fsm:start_link({local, ?MODULE}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% Function: launch/1 %%-------------------------------------------------------------------- %% Start clients with given session list launch({Node, Sessions}) -> ?LOGF("starting on node ~p~n",[[Node]], ?INFO), gen_fsm:send_event({?MODULE, Node}, {launch, Sessions}); % same erlang beam case launch({Node, Host, Sessions}) -> ?LOGF("starting on node ~p~n",[[Node]], ?INFO), gen_fsm:send_event({?MODULE, Node}, {launch, Sessions, atom_to_list(Host)}). %%-------------------------------------------------------------------- %% @spec stop(Node::atom()) -> ok %% @doc Start clients with given session list @end %%-------------------------------------------------------------------- stop(Node) -> ?LOGF("stopping on node ~p~n",[Node], ?INFO), gen_fsm:send_event({?MODULE, Node}, {stop}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, StateName, StateData} | %% {ok, StateName, StateData, Timeout} | %% ignore | %% {stop, StopReason} %%---------------------------------------------------------------------- init([]) -> {ok, MyHostName} = ts_utils:node_to_hostname(node()), ts_launcher_mgr:alive(static), {ok, wait, #state{myhostname=MyHostName}}. %%---------------------------------------------------------------------- %% Func: StateName/2 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- wait({launch, Args, Hostname}, State) -> wait({launch, Args}, State#state{myhostname = Hostname}); %% starting without configuration. We must ask the config server for %% the configuration of this launcher. wait({launch, []}, State) -> MyHostName = State#state.myhostname, ?LOGF("Launch msg receive (~p)~n",[MyHostName], ?NOTICE), ts_launcher_mgr:check_registered(), {ok,Users,Start} = ts_config_server:get_client_config(static,MyHostName), case Users of [{Wait,_}|_] -> Warm = ts_launcher:set_warm_timeout(Start), ts_launcher:set_static_users({node(),length(Users)}), ?LOGF("Activate launcher (~p static users) in ~p msec, first user after ~p ms ~n",[length(Users), Warm, Wait], ?NOTICE), {next_state,launcher,State#state{users = Users}, Warm + Wait}; [] -> ?LOG("No static users, stop",?INFO), ts_launcher:set_static_users({node(),0}), {stop, normal, State} end; wait({stop}, State) -> {stop, normal, State}. launcher(timeout, State=#state{ users = [{OldWait,Session}|Users]}) -> BeforeLaunch = ?NOW, ?LOGF("Launch static user using session ~p ~n", [Session],?DEB), do_launch({Session,State#state.myhostname}), Wait = set_waiting_time(BeforeLaunch, Users, OldWait), ?DebugF("Real Wait =~p ~n", [Wait]), case Users of [] -> ?LOG("no more clients to start ~n",?INFO), {stop, normal, State}; _ -> {next_state,launcher,State#state{users=Users},Wait} end. set_waiting_time(_Before, [] , _Previous) -> 0; % last user set_waiting_time(Before , [{Next,_}|_], Previous) -> LaunchDuration = ts_utils:elapsed(?NOW, Before), %% to keep the rate of new users as expected, remove the time to %% launch a client to the next wait. NewWait = Next - Previous - LaunchDuration, case NewWait > 0 of true -> round(NewWait); false -> 0 end. %%---------------------------------------------------------------------- %% Func: handle_event/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_sync_event/4 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_info/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm %% Returns: any %%---------------------------------------------------------------------- terminate(Reason, _StateName, _StateData) -> ?LOGF("launcher terminating for reason ~p~n",[Reason], ?INFO), ts_launcher_mgr:die(static), ok. %%-------------------------------------------------------------------- %% Func: code_change/4 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%%---------------------------------------------------------------------- %%% Func: do_launch/1 %%%---------------------------------------------------------------------- do_launch({ Session, MyHostName})-> case catch ts_config_server:get_user_param(MyHostName) of {ok, {IPParam, Server, UserId, Dump, Seed}} -> ts_client_sup:start_child(Session#session{client_ip=IPParam,server=Server,userid=UserId, dump=Dump,seed=Seed}), ok; Error -> ?LOGF("get_next_session failed [~p], skip this session !~n", [Error],?ERR), ts_mon_cache:add({ count, error_next_session }), error end. tsung-1.8.0/src/tsung/ts_launcher_mgr.erl0000644000201100017670000002017514377756736020212 0ustar nniclausdream%%% %%% Copyright 2009 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 09 déc. 2009 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_launcher_mgr). -vc('$Id: ts_launcher_mgr.erl,v 0.0 2009/12/09 11:54:33 nniclaus Exp $ '). -author('nicolas.niclausse@niclux.org'). -include("ts_config.hrl"). -include("ts_profile.hrl"). -behaviour(gen_server). %% API -export([start/0, alive/1, die/1, check_registered/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {launchers=0, synced, check_timeout}). %%==================================================================== %% API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). die(Type)-> gen_server:cast(?MODULE, {die, Type}). alive(Type)-> gen_server:cast(?MODULE, {alive, Type}). check_registered()-> gen_server:call(?MODULE, {check_registered}). %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- init([]) -> ?LOG("starting",?INFO), {ok, #state{check_timeout=?check_noclient_timeout}}. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call({check_registered}, _From,State=#state{synced=undefined}) -> %% Check if global names are synced; Annoying "feature" of R10B7 and up case global:registered_names() of ["cport"++_Tail] -> ?LOG("Only cport server registered ! syncing ...~n", ?WARN), global:sync(); [] -> ?LOG("No registered processes ! syncing ...~n", ?WARN), global:sync(); _ -> ok end, ts_mon:launcher_is_alive(), {reply, ok, State#state{synced=yes}}; handle_call({check_registered}, _From,State=#state{synced=yes}) -> ?LOG("syncing already done, skip~n", ?INFO), {reply, ok, State#state{synced=yes}}; handle_call(_Msg, _From, State) -> Reply = ok, {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast({alive, Type}, State=#state{launchers=N}) -> ?LOGF("~p launcher is starting on node ~p ~n",[Type,node()],?DEB), {noreply, State#state{launchers=N+1}}; handle_cast({die, _Type}, State=#state{launchers=1}) -> ?LOGF("All launchers are done on node ~p, wait for active clients to finish~n",[node()],?INFO), ts_config_server:endlaunching(node()), check_clients(State#state{launchers=0}); handle_cast({die, Type}, State=#state{launchers=N}) -> ?LOGF("~p launcher is stopping on node ~p ~n",[Type, node()],?DEB), {noreply, State#state{launchers=N-1}}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info({timeout, _Ref, check_noclient}, State) -> check_clients(State); handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() %% Description: This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any necessary %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> case ts_utils:is_controller() of false -> slave:stop(node()); %% commit suicide. true -> ok end. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- check_clients(State=#state{check_timeout=CheckTimeout}) -> case ts_client_sup:active_clients() of 0 -> % no users left, and no more launchers, stop ?LOGF("No more active users ~p ~p~n",[node(), os:getpid()], ?NOTICE), timer:sleep(?CACHE_DUMP_STATS_INTERVAL+10), %% let ts_mon_cache send it's last stats ts_mon:stop(), %% we must warn ts_mon that our clients have finished case ts_sup:has_cport(node()) of true -> %%do not finish this beam ?LOGF("Beam will not be terminated because it has a cport server ~p ~p~n",[node(), os:getpid()], ?NOTICE), {noreply, State}; false -> {stop, normal, State} end; ActiveClients when ActiveClients > 1000 -> %% the call to active_clients can be cpu hungry if lot's of clients are running %% use a long timer in this case. ?LOGF("Still ~p active client(s)~n", [ActiveClients],?NOTICE), erlang:start_timer(CheckTimeout, self(), check_noclient ), {noreply, State}; ActiveClients -> ?LOGF("Still ~p active client(s)~n", [ActiveClients],?DEB), erlang:start_timer(?fast_check_noclient_timeout, self(), check_noclient ), {noreply, State} end. tsung-1.8.0/src/tsung/ts_launcher.erl0000644000201100017670000005205414377756736017346 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% This module launch clients (ts_client module) given a number of %%% clients and the intensity of the arrival process (intensity = %%% inverse of the mean of inter arrival). The arrival process is a %%% Poisson Process (ie, inter-arrivals are independent and exponential) -module(ts_launcher). -created('Date: 2000/10/23 12:09:57 nniclausse '). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -include("ts_profile.hrl"). -include("ts_config.hrl"). % wait up to 10ms after an error -define(NEXT_AFTER_FAILED_TIMEOUT, 10). -define(DIE_DELAY, 5000). -behaviour(gen_fsm). %% a primitive gen_fsm with two state: launcher and wait %% External exports -export([start/0, launch/1, set_static_users/1]). -export([set_warm_timeout/1]). %% gen_fsm callbacks -export([init/1, launcher/2, wait/2, wait_static/2, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %%-------------------------------------------------------------------- %% Function: start/0 %%-------------------------------------------------------------------- start() -> ?LOG("starting ~n", ?INFO), gen_fsm:start_link({local, ?MODULE}, ?MODULE, [], []). %%-------------------------------------------------------------------- %% Function: launch/1 %%-------------------------------------------------------------------- %% Start clients with given interarrival (can be empty list) launch({Node, Arrivals, Seed}) -> ?LOGF("starting on node ~p~n",[[Node]], ?INFO), gen_fsm:send_event({?MODULE, Node}, {launch, Arrivals, Seed}); % same erlang beam case launch({Node, Host, Arrivals, Seed}) -> ?LOGF("starting on node ~p~n",[[Node]], ?INFO), gen_fsm:send_event({?MODULE, Node}, {launch, Arrivals, atom_to_list(Host), Seed}). %% Start clients with given interarrival (can be empty list) set_static_users({Node,Value}) -> ?LOGF("Subtract static users number to max: ~p~n",[Value], ?DEB), gen_fsm:send_event({?MODULE, Node}, {static, Value}). %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, StateName, StateData} | %% {ok, StateName, StateData, Timeout} | %% ignore | %% {stop, StopReason} %%---------------------------------------------------------------------- init([]) -> {ok, MyHostName} = ts_utils:node_to_hostname(node()), ts_launcher_mgr:alive(dynamic), {ok, wait, #launcher{myhostname=MyHostName}}. %%---------------------------------------------------------------------- %% Func: StateName/2 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- wait({launch, Args, Hostname, Seed}, State) -> wait({launch, Args, Seed}, State#launcher{myhostname = Hostname}); %% starting without configuration. We must ask the config server for %% the configuration of this launcher. wait({launch, [], Seed}, State=#launcher{static_done=Static_done}) -> ts_utils:init_seed(Seed), MyHostName = State#launcher.myhostname, ?LOGF("Launch msg receive (~p)~n",[MyHostName], ?NOTICE), ts_launcher_mgr:check_registered(), case ts_config_server:get_client_config(MyHostName) of {ok, {[NextPhase = #phase{}| Rest], StartDate, Max}} -> ?LOGF("Expected duration of first phase: ~p sec (~p users) ~n",[NextPhase#phase.duration / 1000, NextPhase#phase.nusers], ?NOTICE), check_max_users(Max), NewState = State#launcher{phases = Rest, nusers = NextPhase#phase.nusers, current_phase = NextPhase#phase{id=1}, start_date = StartDate, maxusers=Max }, case Static_done of true -> wait_static({static, 0}, NewState); false -> {next_state,wait_static,NewState} end; {ok,{[],_,_}} -> % no random users, only static. {stop, normal, State} end; %% start with a already known configuration. This case occurs when a %% beam is started by a launcher (maxclients reached) wait({launch, {[NextPhase=#phase{}| Rest], Max, PhaseId}, Seed}, State) -> ?LOGF("Starting with ~p users to do in the current phase (max is ~p)~n", [NextPhase#phase.nusers, Max],?DEB), ts_utils:init_seed(Seed), ?LOGF("Expected duration of phase: ~p sec ~n",[NextPhase#phase.duration / 1000], ?NOTICE), ts_launcher_mgr:check_registered(), {next_state, launcher, State#launcher{phases = Rest, nusers = NextPhase#phase.nusers, current_phase = NextPhase#phase{start=?NOW, id=PhaseId}, maxusers = Max}, State#launcher.short_timeout}; wait({static,0}, State) -> %% static launcher has no work to do, do not wait for him. ?LOG("Wow, static launcher is already sending me a msg, don't forget it ~n", ?INFO), {next_state, wait, State#launcher{static_done=true}}. wait_static({static, _Static}, State=#launcher{nusers=0}) -> %% no users in this phase, next one skip_empty_phase(State); wait_static({static, Static}, State=#launcher{maxusers=Max,current_phase=Phase, nusers=Users,start_date=StartDate}) when is_integer(Static) -> %% add ts_stats:exponential(Intensity) to start time to avoid %% simultaneous start of users when a lot of client beams is %% used. Also, avoid too long delay, so use a maximum delay WarmTimeout = set_warm_timeout(StartDate)+round(ts_stats:exponential(Phase#phase.intensity)), Warm = lists:min([WarmTimeout,?config(max_warm_delay)]), ?LOGF("Activate launcher (~p users) in ~p msec ~n",[Users, Warm], ?NOTICE), PhaseStart = ts_utils:add_time(?NOW, Warm div 1000), NewMax = case Max > Static of true -> Max-Static; false -> ?LOG("Warning: more static users than maximum users per beam !~n",?WARN), 1 % will fork a new beam as soon a one user is started end, ?LOGF("Set maximum users per beam to ~p~n",[NewMax],?DEB), {next_state,launcher,State#launcher{ current_phase = Phase#phase{start = PhaseStart}, maxusers = NewMax }, Warm}. launcher(_Event, State=#launcher{nusers = 0, phases = [] }) -> ?LOG("no more clients to start, stop ~n",?INFO), {stop, normal, State}; launcher(timeout, State=#launcher{nusers = Users, current_phase = Phase, phases = Phases, started_users = Started }) -> BeforeLaunch = ?NOW, Id = Phase#phase.id, case do_launch({Phase#phase.intensity,State#launcher.myhostname, Id}) of {ok, Wait} -> case check_max_raised(State) of true -> %% let the other beam starts and warns ts_mon timer:sleep(?DIE_DELAY), {stop, normal, State}; false-> Duration = ts_utils:elapsed(Phase#phase.start, BeforeLaunch), case change_phase(Users-1, Phases, Duration, Phase) of {change, NextPhase = #phase{nusers = 0}, Rest} -> %% no users in the next phase skip_empty_phase(State#launcher{phases=Rest,current_phase=NextPhase}); {change, NextPhase, Rest} -> ts_mon_cache:add({ count, newphase }), ?LOGF("Start a new arrival phase (~p users, ~p); expected duration=~p sec~n", [NextPhase#phase.nusers, NextPhase#phase.intensity, NextPhase#phase.duration / 1000], ?NOTICE), {next_state,launcher,State#launcher{phases = Rest, nusers = NextPhase#phase.nusers, current_phase = NextPhase#phase{start=?NOW,id=Id+1} }, round(Wait)}; {stop} -> {stop, normal, State}; {continue} -> Now=?NOW, LaunchDuration = ts_utils:elapsed(BeforeLaunch, Now), %% to keep the rate of new users as expected, %% remove the time to launch a client to the next %% wait. NewWait = case Wait > LaunchDuration of true -> trunc(Wait - LaunchDuration); false -> 0 end, ?DebugF("Real Wait = ~p (was ~p)~n", [NewWait,Wait]), {next_state,launcher,State#launcher{nusers = Users-1, started_users=Started+1} , NewWait} end end; error -> % retry with the next user, wait randomly a few msec RndWait = random:uniform(?NEXT_AFTER_FAILED_TIMEOUT), {next_state,launcher,State#launcher{nusers = Users-1} , RndWait} end. %%---------------------------------------------------------------------- %% Func: StateName/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: handle_event/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_sync_event/4 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%---------------------------------------------------------------------- handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_info/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_info(_Info, StateName, StateData) -> {next_state, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm %% Returns: any %%---------------------------------------------------------------------- terminate(Reason, _StateName, _StateData) -> ?LOGF("launcher terminating for reason ~p~n",[Reason], ?INFO), ts_launcher_mgr:die(dynamic), ok. %%-------------------------------------------------------------------- %% Func: code_change/4 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%% @spec skip_empty_phase(record(launcher)) -> {next_state, launcher, record(launcher)} %%% @doc if a phase contains no users, sleep, before trying the next one @end skip_empty_phase(State=#launcher{phases=Phases,current_phase=Phase})-> ?LOGF("No user, skip phase (~p ~p)~n",[Phases,Phase#phase.duration],?INFO), case change_phase(0, Phases, Phase#phase.duration, Phase ) of {stop} -> {stop, normal, State}; {change, NextPhase=#phase{nusers=0}, Rest} -> %% next phase is also empty, loop Id = Phase#phase.id +1, skip_empty_phase(State#launcher{phases=Rest,current_phase=NextPhase#phase{id=Id}}); {change, NextPhase,Rest} -> Id = Phase#phase.id +1, {next_state,launcher,State#launcher{phases = Rest, nusers= NextPhase#phase.nusers, current_phase = NextPhase#phase{id=Id, start=?NOW}},1} end. %%%---------------------------------------------------------------------- %%% Func: change_phase/4 %%% Purpose: decide if we need to change phase (if current users is %%% reached or if max duration is reached) %%% ---------------------------------------------------------------------- change_phase(N, [Phase|Rest], Duration, CurrentPhase = #phase{duration=PhaseDuration}) when N < 1 andalso Duration >= PhaseDuration -> check_sessions_end(CurrentPhase), {change, Phase, Rest}; change_phase(N, [Phase|Rest], Duration, CurrentPhase = #phase{duration=PhaseDuration}) when N < 1 -> %% no more users, check if we need to wait before changing phase (this can happen if maxnumber is set) ToWait=round(PhaseDuration-Duration), ?LOGF("Need to wait ~p sec before changing phase, going to sleep~n", [ToWait/1000], ?WARN), timer:sleep(ToWait), ?LOG("Waking up~n", ?NOTICE), check_sessions_end(CurrentPhase), {change, Phase, Rest}; change_phase(N, [], _, _) when N < 1 -> ?LOG("This was the last phase, wait for connected users to finish their session~n",?NOTICE), {stop}; change_phase(N,NewPhases,Duration, CurrentPhase=#phase{duration=PhaseDuration, nusers=Users}) when Duration>PhaseDuration -> ?LOGF("Check phase: ~p ~p~n",[N,Users],?DEB), Percent = 100*N/Users, case {Percent > ?MAX_PHASE_EXCEED_PERCENT, N > ?MAX_PHASE_EXCEED_NUSERS} of {true,true} -> ?LOGF("Phase duration exceeded, more than ~p% (~.1f%) of users were not launched in time (~p users), tsung may be overloaded !~n", [?MAX_PHASE_EXCEED_PERCENT,Percent,N],?WARN); {_,_} -> ?LOGF("Phase duration exceeded, but not all users were launched (~p users, ~.1f% of phase)~n", [N, Percent],?NOTICE) end, case NewPhases of [NextPhase|Rest] -> check_sessions_end(CurrentPhase), {change, NextPhase,Rest}; [] -> ?LOG("This was the last phase, wait for connected users to finish their session~n",?NOTICE), {stop} end; change_phase(_N, _, _, _) -> {continue}. check_sessions_end(Phase= #phase{wait_all_sessions_end = true}) -> case ts_client_sup:active_clients() of 0 -> ok; ActiveClients when ActiveClients > 1000 -> ?LOGF("Wait for all sessions to finish before starting next phase (still ~p sessions active)", [ActiveClients], ?NOTICE), timer:sleep(?check_noclient_timeout), check_sessions_end(Phase); ActiveClients -> ?LOGF("Wait for all sessions to finish before starting next phase (still ~p sessions active)", [ActiveClients], ?NOTICE), timer:sleep(?fast_check_noclient_timeout), check_sessions_end(Phase) end; check_sessions_end(_) -> ok. %%%---------------------------------------------------------------------- %%% Func: check_max_raised/1 %%%---------------------------------------------------------------------- check_max_raised(State=#launcher{phases=Phases,maxusers=Max,nusers=Users, current_phase = CurrentPhase, started_users=Started }) when Started >= Max-1 -> PendingDuration = CurrentPhase#phase.duration - ts_utils:elapsed(CurrentPhase#phase.start, ?NOW), ActiveClients = ts_client_sup:active_clients(), ?DebugF("Current active clients on beam: ~p (max is ~p)~n", [ActiveClients, State#launcher.maxusers]), case ActiveClients >= Max of true -> ?LOG("Max number of concurrent clients reached, must start a new beam~n", ?NOTICE), Args = case Users of 0 -> Phases; _ -> [CurrentPhase#phase{nusers=Users-1, duration=PendingDuration}|Phases] end, ts_config_server:newbeam(list_to_atom(State#launcher.myhostname), {Args, Max, CurrentPhase#phase.id}), true; false -> ?DebugF("Current clients on beam: ~p~n", [ActiveClients]), false end; check_max_raised(_State) -> % number of started users less than max, no need to check ?DebugF("Current started clients on beam: ~p (max is ~p)~n", [_State#launcher.started_users, _State#launcher.maxusers]), false. %%%---------------------------------------------------------------------- %%% Func: do_launch/1 %%%---------------------------------------------------------------------- do_launch({Intensity, MyHostName, PhaseId})-> %%Get one client %%set the profile of the client case catch ts_config_server:get_next_session({MyHostName, PhaseId} ) of {'EXIT', {timeout, _ }} -> ?LOG("get_next_session failed (timeout), skip this session !~n", ?ERR), ts_mon_cache:add({ count, error_next_session }), error; {ok, Session} -> ts_client_sup:start_child(Session), X = ts_stats:exponential(Intensity), ?DebugF("client launched, wait ~p ms before launching next client~n",[X]), {ok, X}; Error -> ?LOGF("get_next_session failed for unexpected reason [~p], abort !~n", [Error],?ERR), ts_mon_cache:add({ count, error_next_session }), exit(shutdown) end. set_warm_timeout(StartDate)-> case ts_utils:elapsed(?TIMESTAMP, StartDate) of WaitBeforeStart when WaitBeforeStart>0 -> round(WaitBeforeStart); _Neg -> ?LOG("Negative Warm timeout !!! Check if client "++ " machines are synchronized (ntp ?)~n"++ "Anyway, start launcher NOW! ~n", ?WARN), 1 end. check_max_users(Max) -> try Data = os:cmd("grep \"open files\" /proc/self/limits"), {match,[Val]} = re:run(Data,"Max open files\\s+(\\d+)",[{capture,all_but_first,list}]), Limit = list_to_integer(Val), case (Max > Limit ) of true -> ?LOGF("WARNING !!! too few file descriptors available (~w), you should decrease maxusers (currently ~w)",[Limit,Max], ?CRIT); false -> ?LOGF("maxusers is below file descriptors limit (~p)",[Limit], ?DEB) end catch _Error:_Reason -> ?LOG("Can't get file descriptors limit from system, you should verify that 'maxusers' has a good value ",?NOTICE) end. tsung-1.8.0/src/tsung/ts_job.erl0000644000201100017670000002064614377756736016321 0ustar nniclausdream%%% %%% Copyright 2011 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 4 mai 2011 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_job). -author('nicolas.niclausse@inria.fr'). -behaviour(ts_plugin). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_job.hrl"). -include_lib("kernel/include/file.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, dump/2, parse/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). %%==================================================================== %% Data Types %%==================================================================== %% @type dyndata() = #dyndata{proto=ProtoData::term(),dynvars=list()}. %% Dynamic data structure %% @end %% @type server() = {Host::tuple(),Port::integer(),Protocol::atom()}. %% Host/Port/Protocol tuple %% @end %% @type param() = {dyndata(), server()}. %% Dynamic data structure %% @end %% @type hostdata() = {Host::tuple(),Port::integer()}. %% Host/Port pair %% @end %% @type client_data() = binary() | closed. %% Data passed to a protocol implementation is either a binary or the %% atom closed indicating that the server closed the tcp connection. %% @end %%==================================================================== %% API %%==================================================================== parse_config(El,Config) -> ts_config_job:parse_config(El, Config). %% @spec session_defaults() -> {ok, Persistent} | {ok, Persistent, Bidi} %% Persistent = bool() %% Bidi = bool() %% @doc Default parameters for sessions of this protocol. Persistent %% is true if connections are preserved after the underlying tcp %% connection closes. Bidi should be true for bidirectional protocols %% where the protocol module needs to reply to data sent from the %% server. @end session_defaults() -> {ok, true}. % not relevant for erlang type (?). %% @spec new_session() -> State::term() %% @doc Initialises the state for a new protocol session. %% @end new_session() -> #job_session{}. %% @spec decode_buffer(Buffer::binary(),Session::record(job)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#job_session{}) -> Buffer. %% @spec add_dynparams(Subst, dyndata(), param(), hostdata()) -> {dyndata(), server()} | dyndata() %% Subst = term() %% @doc Updates the dynamic request data structure created by %% {@link ts_protocol:init_dynparams/0. init_dynparams/0}. %% @end add_dynparams(false, {_DynVars,Session}, Param, HostData) -> add_dynparams(Session, Param, HostData); add_dynparams(true, {DynVars,Session}, Param, HostData) -> NewParam = subst(Param, DynVars), add_dynparams(Session,NewParam, HostData). add_dynparams(#job_session{}, Param, _HostData) -> Param. %%---------------------------------------------------------------------- %% @spec subst(record(job), term()) -> record(job) %% @doc Replace on the fly dynamic element of the request. %% @end %%---------------------------------------------------------------------- subst(Job=#job{duration=D,req=Req,walltime=WT,resources=Res,options=Opts,jobid=Id}, DynVars) -> Job#job{duration=ts_search:subst(D,DynVars), req=ts_search:subst(Req,DynVars), resources=ts_search:subst(Res,DynVars), walltime=ts_search:subst(WT,DynVars), options=ts_search:subst(Opts,DynVars), jobid=ts_search:subst(Id,DynVars)}. dump(protocol,{none,#job_session{jobid=JobId,owner=Owner,submission_time=Sub,queue_time=Q, start_time=Start,end_time=E,status=Status},Name})-> {R,_}=lists:mapfoldl(fun(A,Acc) -> {integer_to_list(round(ts_utils:elapsed(Acc,A))),A} end,Sub,[Q,Start,E]), Date=integer_to_list(round(ts_utils:time2sec_hires(Sub))), Data=ts_utils:join(";",[JobId,Name,Date]++R++[Status]), ts_mon:dump({protocol, Owner, Data }); dump(_P,_Args) -> ok. %% @spec parse(Data::client_data(), State) -> {NewState, Opts, Close} %% State = #state_rcv{} %% Opts = proplist() %% Close = bool() %% @doc %% Opts is a list of inet:setopts socket options. Don't change the %% active/passive mode here as tsung will set {active,once} before %% your options. %% Setting Close to true will cause tsung to close the connection to %% the server. %% @end parse({os, cmd, _Args, "Admission Rule ERROR" ++ Tail},State=#state_rcv{session=_S})-> ?LOGF("ERROR, admission rule: ~p",[Tail],?WARN), ts_mon_cache:add([{sum,error_job_admission_rule,1}]), {State#state_rcv{ack_done=true,datasize=length(Tail)+21}, [], false}; parse({os, cmd, _Args, Res},State=#state_rcv{session=S,dump=Dump}) when is_list(Res)-> ?LOGF("os:cmd result: ~p",[Res],?DEB), %% oarsub output: %% [ADMISSION RULE] Modify resource description with type constraints %% Generate a job key... %% OAR_JOB_ID=468822 Lines = string:tokens(Res,"\n"), case lists:last(Lines) of "OAR_JOB_ID="++ID -> ?LOGF("OK,job id is ~p",[ID],?INFO), ts_job_notify:monitor({ID,self(),S#job_session.submission_time, ?NOW,Dump}), {State#state_rcv{ack_done=true,datasize=length(Res)}, [], false}; _ -> {State#state_rcv{ack_done=true,datasize=length(Res)}, [], false} end; parse(nojobs,State) -> ?LOGF(" no jobs in queue for ~p, stop waiting",[self()],?DEB), {State#state_rcv{ack_done=true}, [], false}; parse({Mod, Fun, Args, Res},State) -> ?LOGF(" result: ~p",[{Mod, Fun, Args, Res}],?DEB), {State#state_rcv{ack_done=false}, [], false}. %% @spec parse_bidi(Data, State) -> {nodata, NewState} | {Data, NewState} %% Data = client_data() %% NewState = term() %% State = term() %% @doc Parse a block of data from the server. No reply will be sent %% if the return value is nodata, otherwise the Data binary will be %% sent back to the server immediately. %% @end parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data,State). %% @spec get_message(record(job),record(state_rcv)) -> {Message::term(),record(state_rcv)} %% @doc Creates a new message to send to the connected server. %% @end get_message(#job{type=oar,req=wait_jobs},#state_rcv{session=Session}) -> ts_job_notify:wait_jobs(self()), {{erlang, now,[], 0},Session}; % we could use any function call, the result is not used get_message(Job=#job{duration=D},State) when is_integer(D)-> get_message(Job#job{duration=integer_to_list(D)},State); get_message(Job=#job{notify_port=P},State) when is_integer(P)-> get_message(Job#job{notify_port=integer_to_list(P)},State); get_message(#job{type=oar,user=U,req=submit, name=N,script=S, resources=R, queue=Q, walltime=W,notify_port=P, notify_script=NS,duration=D,options=Opts},#state_rcv{session=Session}) -> Submit = case U of undefined -> "oarsub "; User -> "sudo -u "++User++" oarsub " end, Queue = case Q of "" -> ""; _ -> "-q "++ Q end, Cmd=Submit++Queue++" -l "++R++ ",walltime="++W ++" -n " ++N ++" " ++ Opts ++ " " ++" --notify \"exec:" ++NS++" "++P++"\" " ++"\""++S++" "++D++"\"", ?LOGF("Will run ~p",[Cmd],?INFO), Message = {os, cmd, [Cmd], length(Cmd) }, {Message, Session#job_session{submission_time=?NOW}}. tsung-1.8.0/src/tsung/ts_jabber.erl0000644000201100017670000004007614377756736016773 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2004 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. %%% File : ts_jabber.erl %%% Author : Nicolas Niclausse %%% Purpose : Jabber/XMPP plugin %%% Created : 11 Jan 2004 by Nicolas Niclausse -module(ts_jabber). -author('nniclausse@hyperion'). -behavior(ts_plugin). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_jabber.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, subst/2, parse/2, dump/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0, username/2, userid/1]). -export ([starttls_bidi/2, message_bidi/2, presence_bidi/2, ping_bidi/2]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session (persistent & bidirectional) %% Returns: {ok, true|false, true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true, false}. %% @spec decode_buffer(Buffer::binary(),Session::record(jabber)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#jabber_session{}) -> Buffer. % nothing to do for jabber %% @spec userid({Session::record(jabber_session), Dynvars::dynvars()}) -> UID::string() %% @doc return the current userid @end userid({#jabber_session{username=UID},_DynVars})-> UID. %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #jabber_session{}. %%---------------------------------------------------------------------- %% Function: get_message/1 %% Purpose: Build a message/request %% Args: #jabber %% Returns: binary %%---------------------------------------------------------------------- get_message(Req=#jabber{domain={domain,Domain}}, State=#state_rcv{session=S}) when S#jabber_session.domain == undefined -> NewS = S#jabber_session{domain=Domain, user_server=default}, get_message(Req#jabber{domain=Domain, user_server=default},State#state_rcv{session=NewS}); get_message(Req=#jabber{domain={vhost,FileId}}, State=#state_rcv{session=S}) when S#jabber_session.domain == undefined -> {Domain,UserServer} = choose_domain(FileId), NewS = S#jabber_session{domain=Domain, user_server=UserServer}, get_message(Req#jabber{domain=Domain, user_server=UserServer},State#state_rcv{session=NewS}); get_message(Req=#jabber{id=user_defined, username=User, passwd=Passwd}, State=#state_rcv{session=S}) when S#jabber_session.id == undefined -> NewS = S#jabber_session{id=user_defined,username=User,passwd=Passwd}, %% NewDynVars =ts_dynvars:set(xmpp_userid, User, DynData#dyndata.dynvars), %% ?LOGF("Setting up username ~p for ~p~n",[User,ts_dynvars:lookup(tsung_userid,NewDynVars)],?DEB), get_message(Req, State#state_rcv{session=NewS}); get_message(Req=#jabber{prefix=Prefix, passwd=Passwd}, State=#state_rcv{session=S}) when S#jabber_session.id == undefined -> Id = case ts_user_server:get_idle(S#jabber_session.user_server) of {error, no_free_userid} -> ts_mon_cache:add({ count, error_no_free_userid }), exit(no_free_userid); Val-> Val end, {NewUser,NewPasswd} = {username(Prefix,Id), password(Passwd,Id)}, %% NewDynVars =ts_dynvars:set(xmpp_userid, NewUser, DynData#dyndata.dynvars), %% ?LOGF("Setting up username ~p for ~p~n",[NewUser,ts_dynvars:lookup(tsung_userid,NewDynVars)],?DEB), NewS = S#jabber_session{id=Id,username=NewUser,passwd=NewPasswd}, get_message(Req#jabber{username=NewUser,passwd=NewPasswd},State#state_rcv{session=NewS}); get_message(Req=#jabber{},#state_rcv{session=S}) -> {ts_jabber_common:get_message(Req),S}. dump(A,B) -> ts_plugin:dump(A,B). %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: Parse the given data and return a new state %% Args: Data (binary) %% State (record) %% Returns: {NewState, Opts, Close} %% State = #state_rcv{} %% Opts = proplist() %% Close = bool() %%---------------------------------------------------------------------- parse(closed, State) -> ?LOG("XMPP connection closed by server!",?WARN), {State#state_rcv{ack_done = true}, [], true}; parse(Data, State=#state_rcv{datasize=Size}) -> ?DebugF("RECEIVED : ~p~n",[Data]), case get(regexp) of undefined -> ?LOG("No regexp defined, skip",?WARN), {State#state_rcv{ack_done=true}, [], false}; Regexp -> case re:run(Data, Regexp) of {match,_} -> ?DebugF("XMPP parsing: Match (regexp was ~p)~n",[Regexp]), {State#state_rcv{ack_done=true, datasize=Size+size(Data)}, [], false}; nomatch -> {State#state_rcv{ack_done=false,datasize=Size+size(Data)}, [], false} end end. %%---------------------------------------------------------------------- %% Function: parse_bidi/2 %% Purpose: Parse the given data, return a response and new state %% Args: Data (binary) %% State (record) %% Returns: Data (binary) %% NewState (record) %%---------------------------------------------------------------------- parse_bidi(Data, State) -> RcvdXml = binary_to_list(Data), BidiElements = [{"]*subscribe[\"\']", presence_bidi}, {"@@@([^@]+)@@@", message_bidi}, {" case re:run(RcvdXml,Regex) of {match,_} -> ?LOGF("RECEIVED : ~p~n",[RcvdXml],?DEB), ?MODULE:Handler(RcvdXml, State); _Else -> Acc end end, {nodata, State, think}, BidiElements). ping_bidi(RcvdXml, State)-> Fun = fun(Data, Identifier)-> Query = string:concat(Identifier,"='[^']*"), case re:run(Data,Query) of {match,[{Sind, Len}]}-> Data2 = string:substr(Data,Sind+1,Len), string:substr(Data2,string:len(Identifier)+3); _-> nomatch end end, Host = Fun(RcvdXml,"from"), Id = Fun(RcvdXml, "id"), case {Host, Id} of {A, B} when (A =:= nomatch orelse B =:= nomatch) -> ?LOGF("can't find host or id in ping request: ~p",[RcvdXml],?WARN), {nodata, State, think}; {_,_} -> Res = lists:flatten([""]), {list_to_binary(Res),State, think} end. presence_bidi(RcvdXml, State)-> {match,SubMatches} = re:run(RcvdXml,"]*subscribe[\"\'][^>]*>",[global]), bidi_resp(subscribed,RcvdXml,SubMatches,State). message_bidi(RcvdXml, State) -> {match, [NodeStamp]} = re:run(RcvdXml, "@@@([^@]+)@@@", [{capture, all_but_first, list}]), [NodeS, StampS] = string:tokens(NodeStamp, ","), case integer_to_list(erlang:phash2(node())) of NodeS -> [MegaS, SecsS, MicroS] = string:tokens(StampS, ";"), Mega = list_to_integer(MegaS), Secs = list_to_integer(SecsS), Micro = list_to_integer(MicroS), Latency = timer:now_diff(?TIMESTAMP, {Mega, Secs, Micro}), ts_mon_cache:add({ sample, xmpp_msg_latency, Latency / 1000}); _ -> ignore end, {nodata, State, think}. starttls_bidi(_RcvdXml, #state_rcv{socket= Socket, send_timestamp=SendTime}=State)-> ssl:start(), Req = subst(State#state_rcv.request#ts_request.param, State#state_rcv.dynvars), Opt = lists:filter(fun({_,V}) -> V /= undefined end, [{certfile,Req#jabber.certfile}, {keyfile,Req#jabber.keyfile}, {password,Req#jabber.keypass}, {cacertfile,Req#jabber.cacertfile}]), {ok, SSL} = ts_ssl:connect(Socket, Opt), ?LOGF("Upgrading to TLS : ~p",[SSL],?INFO), Latency = ts_utils:elapsed(SendTime, ?NOW), ts_mon_cache:add({ sample, xmpp_starttls, Latency}), {nodata, State#state_rcv{socket=SSL,protocol=ts_ssl}, continue}. %%---------------------------------------------------------------------- %% Function: bidi_resp/4 %% Purpose: Parse XMPP packet, build client response %% Accommodates single packets w/ multiple requests %% Args: RcvdXml (list) %% Submatches (list) %% State (record) %% Returns: Data (binary) %% NewState (record) %% think|continue %%---------------------------------------------------------------------- %% subscribed: Complete a pending subscription request bidi_resp(subscribed,RcvdXml,SubMatches,State) -> JoinedXml=lists:foldl(fun(X,Foo) -> [{Start,Len}]=X, SubStr = string:substr(RcvdXml,Start+1,Len), case re:run(SubStr,"from=[\"']([^\s]*)[\"'][\s\/\>]",[{capture,[1],list}]) of {match,[MyId]} -> %% MyId=string:substr(SubStr,Start1 +6, Length1 -8), ?LOGF("Subscription request from : ~p~n",[MyId],?DEB), MyXml = [""], lists:append([Foo],[MyXml]); _Else -> ?LOGF("Error getting sender address: ~p~n",[SubStr],?DEB), "" end end,"",SubMatches), case lists:flatten(JoinedXml) of "" -> {nodata,State, think}; _ -> ?LOGF("RESPONSE TO SEND : ~s~n",[JoinedXml],?DEB), {list_to_binary(JoinedXml),State, think} end. %% parse_config(Element, Conf) -> ts_config_jabber:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %%---------------------------------------------------------------------- %% The rest of the code expect to found a "domain" field in the #jabber request %% with the domain of the jabber server (as string). We use the step of dynvars substitution %% to choose and set the domain we want to connect, and keep that choice in the %% process dictionary so we reuse it for all request made from the same session. %% (see comments on choose_domain/1 %% %% if we are testing a single domain (the default case), we change from {domain,D}. %% to the specified domain (D). If {vhost,FileId}, we choose a domain from that file %% and set it. %% first request in a session, do nothing add_dynparams(Subst, {DynVars, S}, Param=#jabber{}, Host) when S#jabber_session.id == undefined -> add_dynparams2(Subst,DynVars, Param, Host); add_dynparams(Subst, {DynVars, S}, Param=#jabber{}, Host) -> add_dynparams2(Subst,DynVars, Param#jabber{id=S#jabber_session.id, username=S#jabber_session.username, passwd=S#jabber_session.passwd, domain=S#jabber_session.domain, user_server=S#jabber_session.user_server},Host). add_dynparams2(false,_, Param, _Host) -> Param; add_dynparams2(true, DynVars, Param, _Host) -> ?DebugF("Subst in jabber msg (~p) with dyn vars ~p~n",[Param,DynVars]), NewParam = subst(Param, DynVars), updatejab(DynVars, NewParam). %% This isn't ideal.. but currently there is no other way %% than use side effects, as get_message/1 andn add_dynparams/4 aren't allowed %% to return a new DynData, and so they can't modify the session state. choose_domain(VHostFileId) -> {ok,DomainBin} = ts_file_server:get_random_line(VHostFileId), Domain=binary_to_list(DomainBin), UserServer = global:whereis_name(list_to_atom("us_"++Domain)), {Domain,UserServer}. %%---------------------------------------------------------------------- %% Function: subst/2 %% Purpose: Replace on the fly dynamic element %%---------------------------------------------------------------------- subst(Req=#jabber{id=user_defined, username=Name,passwd=Pwd, data=Data, resource=Resource}, Dynvars) -> NewUser = ts_search:subst(Name,Dynvars), NewPwd = ts_search:subst(Pwd,Dynvars), NewData = ts_search:subst(Data,Dynvars), subst2(Req#jabber{username=NewUser,passwd=NewPwd,data=NewData,resource=ts_search:subst(Resource,Dynvars)}, Dynvars); subst(Req=#jabber{data=Data,resource=Resource}, Dynvars) -> subst2(Req#jabber{data=ts_search:subst(Data,Dynvars),resource=ts_search:subst(Resource,Dynvars)},Dynvars). subst2(Req=#jabber{type = Type}, Dynvars) when Type == 'starttls' -> Req#jabber{cacertfile = ts_search:subst(Req#jabber.cacertfile, Dynvars), keyfile = ts_search:subst(Req#jabber.keyfile, Dynvars), keypass = ts_search:subst(Req#jabber.keypass, Dynvars), certfile = ts_search:subst(Req#jabber.certfile, Dynvars)}; subst2(Req=#jabber{type = Type}, Dynvars) when Type == 'muc:chat' ; Type == 'muc:join'; Type == 'muc:nick' ; Type == 'muc:exit' -> Req#jabber{nick = ts_search:subst(Req#jabber.nick, Dynvars), room = ts_search:subst(Req#jabber.room, Dynvars)}; subst2(Req=#jabber{type = Type}, Dynvars) when Type == 'pubsub:create' ; Type == 'pubsub:subscribe'; Type == 'pubsub:publish'; Type == 'pubsub:delete' -> Req#jabber{node = ts_search:subst(Req#jabber.node, Dynvars)}; subst2(Req=#jabber{type = Type}, Dynvars) when Type == 'pubsub:unsubscribe' -> NewNode=ts_search:subst(Req#jabber.node,Dynvars), NewSubId=ts_search:subst(Req#jabber.subid,Dynvars), Req#jabber{node=NewNode,subid=NewSubId}; subst2(Req, _Dynvars) -> Req. %%---------------------------------------------------------------------- %% Func: updatejab/2 %% takes dyn vars and adds them to jabber record %% 'nonce' used for sip-digest auth %% 'sid' session-id used for digest auth %%---------------------------------------------------------------------- updatejab(undefined,Param) -> Param; updatejab([],Param) -> Param; updatejab([{nonce, Val}|Rest], Param)-> updatejab(Rest, Param#jabber{nonce = Val}); updatejab([{sid, Val}|Rest], Param)-> updatejab(Rest, Param#jabber{sid = Val}); updatejab([_|Rest], Param)-> updatejab(Rest, Param). %%%---------------------------------------------------------------------- %%% Func: username/2 %%% Generate the username given a prefix and id %%%---------------------------------------------------------------------- username(Prefix, DestId) when is_integer(DestId)-> Prefix ++ integer_to_list(DestId); username(Prefix, DestId) -> Prefix ++ DestId. %%%---------------------------------------------------------------------- %%% Func: password/1 %%% Generate password for a given username %%%---------------------------------------------------------------------- password(Prefix,Id) -> username(Prefix,Id). tsung-1.8.0/src/tsung/ts_jabber_common.erl0000644000201100017670000010760614377756736020346 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_jabber_common). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -export([ get_message/1, starttls/0 ]). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_jabber.hrl"). %%---------------------------------------------------------------------- %% Func: get_message/1 %% Args: #jabber record %% Returns: binary %% Purpose: Build a message/request from a #jabber record %%---------------------------------------------------------------------- get_message(Jabber=#jabber{regexp=RegExp}) when RegExp /= undefined-> put(regexp, RegExp), get_message(Jabber#jabber{regexp=undefined}); get_message(_Jabber=#jabber{type = 'wait'}) -> << >>; get_message(Jabber=#jabber{id=user_defined, username=User,passwd=Pwd,type = 'connect'}) -> ts_user_server:add_to_connected({User,Pwd}), connect(Jabber); get_message(Jabber=#jabber{type = 'connect'}) -> connect(Jabber); get_message(#jabber{type = 'starttls'}) -> starttls(); get_message(#jabber{type = 'close', id=Id,username=User,passwd=Pwd,user_server=UserServer,version=Version}) -> ts_user_server:remove_connected(UserServer,set_id(Id,User,Pwd)), close(Version); get_message(#jabber{type = 'presence'}) -> presence(); get_message(#jabber{type = 'presence:initial', id=Id,username=User,passwd=Pwd,user_server=UserServer}) -> ts_user_server:add_to_online(UserServer,set_id(Id,User,Pwd)), presence(); get_message(#jabber{type = 'presence:final', id=Id,username=User,passwd=Pwd,user_server=UserServer}) -> ts_user_server:remove_from_online(UserServer,set_id(Id,User,Pwd)), presence(unavailable); get_message(#jabber{type = 'presence:broadcast', show=Show, status=Status}) -> presence(broadcast, Show, Status); get_message(Jabber=#jabber{type = 'presence:directed', id=Id,username=User,passwd=Pwd,prefix=Prefix, show=Show, status=Status,user_server=UserServer}) -> case ts_user_server:get_online(UserServer,set_id(Id,User,Pwd)) of {ok, {Dest,_}} -> presence(directed, Dest, Jabber, Show, Status); {ok, Dest} -> presence(directed, ts_jabber:username(Prefix,Dest), Jabber, Show, Status); {error, no_online} -> ts_mon_cache:add({ count, error_no_online }), << >> end; get_message(Jabber=#jabber{dest=previous}) -> Dest = get(previous), get_message(Jabber#jabber{dest=Dest}); get_message(Jabber=#jabber{type = 'presence:roster'}) -> presence(roster, Jabber); get_message(#jabber{type = 'presence:subscribe'}) -> %% must be called AFTER iq:roster:add case get(rosterjid) of undefined -> ?LOG("Warn: no jid set for presence subscribe, skip",?WARN), <<>>; RosterJid -> presence(subscribe, RosterJid) end; get_message(Jabber=#jabber{type = 'chat', id=Id, dest=online,username=User,passwd=Pwd, prefix=Prefix, domain=Domain,user_server=UserServer})-> case ts_user_server:get_online(UserServer,set_id(Id,User,Pwd)) of {ok, {Dest,_}} -> message(Dest, Jabber, Domain); {ok, Dest} -> message(ts_jabber:username(Prefix,Dest), Jabber, Domain); {error, no_online} -> ts_mon_cache:add({ count, error_no_online }), << >> end; get_message(Jabber=#jabber{type = 'chat',domain=Domain,prefix=Prefix,dest=offline,user_server=UserServer})-> case ts_user_server:get_offline(UserServer) of {ok, {Dest,_}} -> message(Dest, Jabber, Domain); {ok, Dest} -> message(ts_jabber:username(Prefix,Dest), Jabber, Domain); {error, no_offline} -> ts_mon_cache:add({ count, error_no_offline }), << >> end; get_message(Jabber=#jabber{type = 'chat', dest=random, prefix=Prefix, domain=Domain,user_server=UserServer}) -> case ts_user_server:get_id(UserServer) of {error, Msg} -> ?LOGF("Can't find a random user (~p)~n", [Msg],?ERR), << >>; {Dest,_} -> message(Dest, Jabber, Domain); DestId -> message(ts_jabber:username(Prefix,DestId), Jabber, Domain) end; get_message(Jabber=#jabber{type = 'chat', dest=unique, prefix=Prefix, domain=Domain,user_server=UserServer})-> case ts_user_server:get_first(UserServer) of {Dest, _} -> message(Dest, Jabber, Domain); IdDest -> message(ts_jabber:username(Prefix,IdDest), Jabber, Domain) end; get_message(_Jabber=#jabber{type = 'chat', id=_Id, dest = undefined, domain=_Domain}) -> %% this can happen if previous is set but undefined, skip ts_mon_cache:add({ count, error_no_previous }), << >>; get_message(Jabber=#jabber{type = 'chat', id=_Id, dest = Dest, domain=Domain}) -> ?DebugF("~w -> ~w ~n", [_Id, Dest]), message(Dest, Jabber, Domain); get_message(#jabber{type = 'iq:roster:add', id=Id, dest = online, username=User,passwd=Pwd, domain=Domain, group=Group,user_server=UserServer, prefix=Prefix}) -> case ts_user_server:get_online(UserServer,set_id(Id,User,Pwd)) of {ok, {Dest,_}} -> request(roster_add, Domain, Dest, Group); {ok, DestId} -> request(roster_add, Domain, ts_jabber:username(Prefix,DestId), Group); {error, no_online} -> ts_mon_cache:add({ count, error_no_online }), << >> end; get_message(#jabber{type = 'iq:roster:add',dest = offline, prefix=Prefix, domain=Domain, group=Group, user_server=UserServer})-> case ts_user_server:get_offline(UserServer) of {ok, {Dest,_}} -> request(roster_add, Domain, Dest, Group); {ok, Dest} -> request(roster_add, Domain, ts_jabber:username(Prefix,Dest), Group); {error, no_offline} -> ts_mon_cache:add({ count, error_no_offline }), << >> end; get_message(#jabber{type = 'iq:roster:rename', group=Group})-> %% must be called AFTER iq:roster:add case get(rosterjid) of undefined -> ?LOG("Warn: no jid set for iq:roster:rename msg, skip",?WARN), <<>>; RosterJid -> request(roster_rename, RosterJid, Group) end; get_message(#jabber{type = 'iq:roster:remove'})-> %% must be called AFTER iq:roster:add case get(rosterjid) of undefined -> ?LOG("Warn: no jid set for iq:roster:remove msg, skip",?WARN), <<>>; RosterJid -> request(roster_remove, RosterJid) end; get_message(#jabber{type = 'iq:roster:get', id = Id,username=User,domain=Domain}) -> request(roster_get, User, Domain, Id); get_message(Jabber=#jabber{type = 'raw'}) -> raw(Jabber); %% -- Pubsub benchmark support -- %% For node creation, data contains the pubsub nodename (relative to user %% hierarchy or absolute, optional) get_message(#jabber{type = 'pubsub:create', username=Username, node=Node, node_type=NodeType, data = Data, pubsub_service = PubSubComponent, domain = Domain}) -> create_pubsub_node(Domain, PubSubComponent, Username, Node, NodeType, Data); get_message(#jabber{type = 'pubsub:delete', username=Username, node=Node, pubsub_service = PubSubComponent, domain = Domain}) -> delete_pubsub_node(Domain, PubSubComponent, Username, Node); %% For node subscription, data contain the pubsub nodename (relative to user %% hierarchy or absolute) get_message(#jabber{type = 'pubsub:subscribe', id=Id, username=UserFrom, user_server=UserServer, passwd=Pwd, prefix=Prefix, dest=online, node=Node, pubsub_service = PubSubComponent, domain = Domain}) -> case ts_user_server:get_online(UserServer,set_id(Id,UserFrom,Pwd)) of {ok, {UserTo,_}} -> subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node); {ok, Dest} -> UserTo = ts_jabber:username(Prefix, Dest), %%FIXME: we need the username prefix here subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node); {error, no_online} -> ts_mon_cache:add({ count, error_no_online }), << >> end; get_message(#jabber{type = 'pubsub:subscribe', username=UserFrom, user_server=UserServer, prefix=Prefix, dest=offline, node=Node, domain = Domain, pubsub_service = PubSubComponent}) -> case ts_user_server:get_offline(UserServer) of {ok, {UserTo,_}} -> subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node); {ok, DestId} -> UserTo = ts_jabber:username(Prefix,DestId), subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node); {error, no_offline} -> ts_mon_cache:add({ count, error_no_offline }), << >> end; get_message(#jabber{type = 'pubsub:subscribe', username=UserFrom, user_server=UserServer, prefix=Prefix, dest=random, node=Node, domain = Domain, pubsub_service = PubSubComponent}) -> case ts_user_server:get_id(UserServer) of {UserTo,_} -> subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node); DestId -> UserTo = ts_jabber:username(Prefix,DestId), subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node) end; get_message(#jabber{type = 'pubsub:subscribe', username=UserFrom, dest=UserTo, node=Node, domain = Domain, pubsub_service = PubSubComponent}) -> subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node); %% FIXME is it ok ?! %% For node unsubscribe, data contain the pubsub nodename (relative to user %% hierarchy or absolute) get_message(#jabber{type = 'pubsub:unsubscribe', username=UserFrom, user_server=UserServer, prefix=Prefix, dest=random, node=Node, domain=Domain, pubsub_service=PubSubComponent, subid=SubId}) -> case ts_user_server:get_id(UserServer) of {UserTo,_} -> unsubscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node, SubId); DestId -> UserTo = ts_jabber:username(Prefix,DestId), unsubscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node, SubId) end; get_message(#jabber{type = 'pubsub:unsubscribe', username=UserFrom, dest=UserTo, node=Node, domain=Domain, pubsub_service=PubSubComponent, subid=SubId}) -> unsubscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node, SubId); %% For node publication, data contain the pubsub nodename (relative to user %% hierarchy or absolute) get_message(#jabber{type = 'pubsub:publish', size=Size, username=Username, stamped=Stamped, node=Node, pubsub_service=PubSubComponent, domain=Domain}) -> publish_pubsub_node(Domain, PubSubComponent, Username, Node, Size, Stamped); %% MUC benchmark support get_message(#jabber{type = 'muc:join', room = Room, nick = Nick, muc_service = Service }) -> muc_join(Room,Nick, Service); get_message(#jabber{type = 'muc:chat', room = Room, muc_service = Service, size = Size, stamped = Stamped}) -> muc_chat(Room, Service, Size, Stamped); get_message(#jabber{type = 'muc:nick', room = Room, muc_service = Service, nick = Nick}) -> muc_nick(Room, Nick, Service); get_message(#jabber{type = 'muc:exit', room = Room, muc_service = Service, nick = Nick}) -> muc_exit(Room, Nick, Service); get_message(#jabber{type = 'ping', data=Data, id=Id, dest=online, username=Username, passwd=Pwd, resource=Resource, domain=Domain, user_server=UserServer, prefix=Prefix}) -> case ts_user_server:get_online(UserServer,set_id(Id,Username,Pwd)) of {ok, {Dest,_}} -> ping(Domain, Username, Resource, Data, Dest); {ok, DestId} -> ping(Domain, Username, Resource, Data, ts_jabber:username(Prefix,DestId)); {error, no_online} -> ts_mon_cache:add({ count, error_no_online }), << >> end; get_message(#jabber{type = 'ping', data=Data, username = Username, domain = Domain, resource=Resource}) -> ping(Domain, Username, Resource, Data, undefined); get_message(Jabber=#jabber{id=user_defined}) -> get_message2(Jabber); %% Privacy lists benchmark support get_message(#jabber{type = 'privacy:get_names', username = Name, domain = Domain}) -> privacy_get_names(Name, Domain); get_message(#jabber{type = 'privacy:set_active', username = Name, domain = Domain}) -> privacy_set_active(Name, Domain); get_message(Jabber) -> get_message2(Jabber). %%---------------------------------------------------------------------- %% Func: get_message2/1 %%---------------------------------------------------------------------- get_message2(Jabber=#jabber{type = 'register'}) -> registration(Jabber); get_message2(Jabber=#jabber{type = 'auth_get'}) -> auth_get(Jabber); get_message2(Jabber=#jabber{type = 'auth_set_plain'}) -> auth_set_plain(Jabber); get_message2(Jabber=#jabber{type = 'auth_set_digest', sid=Sid}) -> auth_set_digest(Jabber,Sid); get_message2(Jabber=#jabber{type = 'auth_set_sip', domain=Realm, nonce=Nonce}) -> auth_set_sip(Jabber,Nonce,Realm); get_message2(Jabber=#jabber{type = 'auth_sasl'}) -> auth_sasl(Jabber,"PLAIN"); get_message2(Jabber=#jabber{type = 'auth_sasl_anonymous'}) -> auth_sasl(Jabber,"ANONYMOUS"); get_message2(Jabber=#jabber{type = 'auth_sasl_external'}) -> auth_sasl(Jabber,"EXTERNAL"); get_message2(Jabber=#jabber{type = 'auth_sasl_bind'}) -> auth_sasl_bind(Jabber); get_message2(Jabber=#jabber{type = 'auth_sasl_session'}) -> auth_sasl_session(Jabber). %%---------------------------------------------------------------------- %% Func: connect/1 %%---------------------------------------------------------------------- connect(#jabber{domain=Domain, version="websocket"}) -> list_to_binary([ ""]); connect(#jabber{domain=Domain, version = Version}) -> VersionStr = case Version of "legacy" -> ""; V when is_list(V) -> "version='" ++ Version ++"' " end, list_to_binary([ ""]). %%---------------------------------------------------------------------- %% Func: close/1 %% Purpose: close jabber session %%---------------------------------------------------------------------- close("websocket") -> <<"">>; close(_Version) -> <<"">>. %%---------------------------------------------------------------------- %% Func: starttls/0 %% Purpose: send the starttls element %%---------------------------------------------------------------------- starttls()-> <<"">>. %%---------------------------------------------------------------------- %% Func: auth_get/1 %%---------------------------------------------------------------------- auth_get(#jabber{username=Name,passwd=Passwd})-> auth_get(Name, Passwd, "auth"). %%---------------------------------------------------------------------- %% Func: auth_get/3 %%---------------------------------------------------------------------- auth_get(Username, _Passwd, Type) -> list_to_binary([ "", "", "", Username, ""]). %%---------------------------------------------------------------------- %% Func: auth_set_plain/1 %%---------------------------------------------------------------------- auth_set_plain(#jabber{username=Name,passwd=Passwd,resource=Resource})-> auth_set_plain(Name, Passwd, "auth", Resource). %%---------------------------------------------------------------------- %% Func: auth_set_plain/3 %%---------------------------------------------------------------------- auth_set_plain(Username, Passwd, Type, Resource) -> list_to_binary([ "", "", "", Username, "", "", Resource,"", "", Passwd, ""]). %%---------------------------------------------------------------------- %% Func: auth_set_digest/2 %%---------------------------------------------------------------------- auth_set_digest(#jabber{username=Name,passwd=Passwd, resource=Resource}, Sid)-> auth_set_digest(Name, Passwd, "auth", Sid, Resource). %%---------------------------------------------------------------------- %% Func: auth_set_digest/4 %%---------------------------------------------------------------------- auth_set_digest(Username, Passwd, Type, Sid, Resource) -> {Digest} = ts_digest:digest(Sid, Passwd), list_to_binary([ "", "", "", Username, "", "",Resource,"", "", Digest, ""]). %%---------------------------------------------------------------------- %% Func: auth_set_sip/3 %%---------------------------------------------------------------------- auth_set_sip(#jabber{username=Name,passwd=Passwd,domain=Domain,resource=Resource}, Nonce, Realm)-> auth_set_sip(Name, Passwd, Domain, "auth", Nonce, Realm,Resource). %%---------------------------------------------------------------------- %% Func: auth_set_sip/6 %%---------------------------------------------------------------------- auth_set_sip(Username, Passwd, Domain, Type, Nonce, Realm,Resource) -> Jid = Username ++ "@" ++ Realm, {SipDigest,Integrity} = ts_digest:sip_digest(Nonce, Jid, Realm, Passwd), list_to_binary([ "", "", "", Jid, "", "",Resource,"", "", "", Domain, "", "", "", Jid, "", "", SipDigest, "", "", Nonce, "", "", Integrity, "", ""]). %%---------------------------------------------------------------------- %% Func: auth_sasl/1 %%---------------------------------------------------------------------- auth_sasl(_,"ANONYMOUS")-> list_to_binary([""]); auth_sasl(_,"EXTERNAL")-> list_to_binary(["="]); auth_sasl(#jabber{username=Name,passwd=Passwd},Mechanism)-> auth_sasl(Name, Passwd, Mechanism). %%---------------------------------------------------------------------- %% Func: auth_sasl/2 %%---------------------------------------------------------------------- auth_sasl(Username, Passwd, Mechanism) -> S = <<0>>, N = list_to_binary(Username), P = list_to_binary(Passwd), list_to_binary(["", base64:encode(<>) ,""]). %%---------------------------------------------------------------------- %% Func: auth_sasl_bind/1 %%---------------------------------------------------------------------- auth_sasl_bind(#jabber{username=Name,passwd=Passwd,domain=Domain, resource=Resource})-> auth_sasl_bind(Name, Passwd, Domain, Resource). %%---------------------------------------------------------------------- %% Func: auth_sasl_bind/3 %%---------------------------------------------------------------------- auth_sasl_bind(_Username, _Passwd, _Domain, Resource) -> list_to_binary(["", "",Resource,"", ""]). %%---------------------------------------------------------------------- %% Func: auth_sasl_session/1 %%---------------------------------------------------------------------- auth_sasl_session(#jabber{username=Name,passwd=Passwd,domain=Domain})-> auth_sasl_session(Name, Passwd, Domain). %%---------------------------------------------------------------------- %% Func: auth_sasl_session/3 %%---------------------------------------------------------------------- auth_sasl_session(_Username, _Passwd, _Domain) -> list_to_binary([""]). %%---------------------------------------------------------------------- %% Func: registration/1 %% Purpose: register message %%---------------------------------------------------------------------- registration(#jabber{username=Name,passwd=Passwd,resource=Resource})-> auth_set_plain(Name, Passwd, "register",Resource). %%---------------------------------------------------------------------- %% Func: message/3 %% Purpose: send message to defined user at the Service (aim, ...) %%---------------------------------------------------------------------- message(Dest, #jabber{size=Size,data=undefined,stamped=Stamped}, Service) when is_integer(Size) -> put(previous, Dest), list_to_binary([ "",maybe_stamp(Stamped, Size), ""]); message(Dest, #jabber{data=Data}, Service) when is_list(Data) -> put(previous, Dest), list_to_binary([ "",Data, ""]). %%---------------------------------------------------------------------- %% Func: presence/0 %%---------------------------------------------------------------------- presence() -> list_to_binary([ ""]). %%---------------------------------------------------------------------- %% Func: presence/1 %%---------------------------------------------------------------------- presence(unavailable)-> list_to_binary([ ""]). %%---------------------------------------------------------------------- %% Func: presence/2 %%---------------------------------------------------------------------- presence(roster, Jabber)-> presence(subscribed, Jabber); presence(subscribe, RosterJid)-> list_to_binary([ ""]); presence(Type, Jabber) when is_atom(Type)-> presence(atom_to_list(Type), Jabber); presence(Type, #jabber{dest=DestName, domain=Domain})-> list_to_binary([ ""]). %%---------------------------------------------------------------------- %% Func: presence/3 %%---------------------------------------------------------------------- presence(broadcast, Show, Status) -> list_to_binary([ "", "", Show, "", Status, ""]). %%---------------------------------------------------------------------- %% Func: presence/4 %%---------------------------------------------------------------------- presence(directed, DestName, #jabber{domain=Domain}, Show, Status) -> list_to_binary([ "", "", Show, "", Status, ""]). %%---------------------------------------------------------------------- %% Func: request/3 %%---------------------------------------------------------------------- request(roster_rename, RosterJid,Group) -> list_to_binary([ "", Group, ""]). request(roster_remove, RosterJid) -> list_to_binary([ ""]). %%---------------------------------------------------------------------- %% Func: request/4 %%---------------------------------------------------------------------- request(roster_add, Domain, Dest, Group)-> RosterJid = Dest ++ "@" ++ Domain, _ = put(rosterjid,RosterJid), list_to_binary([ "","",Group,""]); %% Func: request/4 request(roster_get, _UserName, _Domain, _Id)-> list_to_binary([ ""]). %%%---------------------------------------------------------------------- %%% Func: raw/1 %%%---------------------------------------------------------------------- raw(#jabber{data=undefined}) -> << >>; raw(#jabber{data=Data}) when is_list(Data) -> list_to_binary(Data). %%%---------------------------------------------------------------------- %%% Func: create_pubsub_node/5 %%% Create a pubsub node: Generate XML packet %%% If node name is undefined (data attribute), we create a pubsub instant %%% node. %%% Nodenames are relative to the User pubsub hierarchy (ejabberd); they are %%% absolute with leading slash. %%%---------------------------------------------------------------------- create_pubsub_node(Domain, PubSubComponent,Username, Node, NodeType, Data) -> list_to_binary(["" "" "", " ", create_pubsub_node_options(Data), ""]). create_pubsub_node_options(undefined) -> ""; create_pubsub_node_options(Data) when is_list(Data) -> case erl_scan:string(Data) of {ok, Ts, _} -> field_elements(erl_parse:parse_term(Ts)); _ -> ?LOG("Warn: Invalid erlang term scanned from data in pubsub create node", ?WARN), "" end. field_value(Value) when is_list(Value) -> F = fun(Item, Acc) -> Acc ++ "" ++ atom_to_list(Item) ++ "" end, lists:foldl(F, "", Value); field_value(Value) -> "" ++ atom_to_list(Value) ++ "". field_elements({ok, Fields}) -> F = fun({Field, Value}, Acc) -> Acc ++ "" ++ field_value(Value) ++ "" end, lists:foldl(F, "", Fields); field_elements(_) -> ?LOG("Warn: Invalid erlang term parsed from data in pubsub create node", ?WARN), "". %% Generate pubsub node attribute pubsub_node_attr(undefined, _Domain, _Username) -> " "; pubsub_node_attr(user_root, Domain, Username) -> [" node='/home/", Domain, "/", Username,"'"]; pubsub_node_attr([$/|AbsNode], _Domain, _Username) -> [" node='/", AbsNode,"'"]; pubsub_node_attr(Node, Domain, Username) -> [" node='/home/", Domain, "/", Username, "/", Node,"'"]. pubsub_node_type(undefined) -> ""; pubsub_node_type(Type) when is_list(Type) -> [" type='", Type, "' "]. pubsub_subid(undefined) -> ""; pubsub_subid(SubId) when is_list(SubId) -> [" subid='", SubId, "' "]. %%%---------------------------------------------------------------------- %%% Func: delete_pubsub_node/4 %%% Delete a pubsub node: Generate XML packet %%% Nodenames are relative to the User pubsub hierarchy (ejabberd); they are %%% absolute with leading slash. %%%---------------------------------------------------------------------- delete_pubsub_node(Domain, PubSubComponent,Username, Node) -> list_to_binary(["" "" ""]). %%%---------------------------------------------------------------------- %%% Func: subscribe_pubsub_node/4 %%% Subscribe to a pubsub node: Generate XML packet %%% If node name is undefined (data attribute), we subscribe to target user %%% root node %%% Nodenames are relative to the User pubsub hierarchy (ejabberd); they are %%% absolute with leading slash. %%%---------------------------------------------------------------------- subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, undefined) -> subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, ""); subscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node) -> list_to_binary(["" "" "" ""]). %%%---------------------------------------------------------------------- %%% Func: unsubscribe_pubsub_node/4 %%% Unsubscribe from a pubsub node: Generate XML packet %%% If node name is undefined (data attribute), we unsubscribe from target user %%% root node %%% Nodenames are relative to the User pubsub hierarchy (ejabberd); they are %%% absolute with leading slash. %%%---------------------------------------------------------------------- unsubscribe_pubsub_node(Domain, PubSubComponent, UserFrom, UserTo, Node, SubId) -> list_to_binary(["" "" "", "", ""]). %%%---------------------------------------------------------------------- %%% Func: publish_pubsub_node/4 %%% Publish an item to a pubsub node %%% Nodenames are relative to the User pubsub hierarchy (ejabberd); they are %%% absolute with leading slash. %%%---------------------------------------------------------------------- publish_pubsub_node(Domain, PubSubComponent, Username, Node, Size, Stamped) -> Result = list_to_binary(["" "" "" "", maybe_stamp(Stamped, Size),"" ""]), Result. muc_join(Room,Nick, Service) -> Result = list_to_binary(["", "", " "]), Result. muc_chat(Room, Service, Size, Stamped) -> Result = list_to_binary(["", "", maybe_stamp(Stamped, Size), "", ""]), Result. muc_nick(Room, Nick, Service) -> Result = list_to_binary([""]), Result. muc_exit(Room,Nick, Service) -> Result = list_to_binary([""]), Result. %%%---------------------------------------------------------------------- %%% Func: privacy_get_names/2 %%% Get names of all privacy lists server stores for the user %%%---------------------------------------------------------------------- privacy_get_names(User, Domain) -> Jid = [User,"@",Domain,"/tsung"], Req = ["", "", ""], list_to_binary(Req). %%%---------------------------------------------------------------------- %%% Func: privacy_set_active/2 %%% Set the list named according to pattern "@_list" %%% as active %%%---------------------------------------------------------------------- privacy_set_active(User, Domain) -> Jid = [User,"@",Domain,"/tsung"], List = [User,"@",Domain,"_list"], Req = ["", "", "", "", ""], list_to_binary(Req). % Resource is not reliably tracked by tsung ping(Domain, Username, _Resource, Data, Dest) -> %Jid = [Username, "@", Domain, "/", Resource], Jid = [Username, "@", Domain], ToJid = case Dest of undefined -> Domain; _ -> [Dest, "@", Domain] end, Id = case Data of undefined -> ["ping1"]; _ -> Data end, list_to_binary([""]). %% set the real Id; by default use the Id; but it user and passwd is %% defined statically (using csv for example), Id is the tuple { User, Passwd } set_id(user_defined,User,Passwd) -> {User,Passwd}; set_id(Id,_User,_Passwd) -> Id. maybe_stamp(Stamped, Size)-> Stamp = generate_stamp(Stamped), PadLen = Size - length(Stamp), Data = case PadLen > 0 of true -> ts_utils:urandomstr_noflat(PadLen); false -> "" end, Stamp ++ Data. generate_stamp(false) -> ""; generate_stamp(true) -> {Mega, Secs, Micro} = ?TIMESTAMP, TS = integer_to_list(Mega) ++ ";" ++ integer_to_list(Secs) ++ ";" ++ integer_to_list(Micro), "@@@" ++ integer_to_list(erlang:phash2(node())) ++ "," ++ TS ++ "@@@". tsung-1.8.0/src/tsung/ts_ip_scan.erl0000644000201100017670000001674214377756736017165 0ustar nniclausdream%%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% Created : 9 Aug 2010 by Nicolas Niclausse %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%%------------------------------------------------------------------- %%% @author Nicolas Niclausse %%% @copyright (C) 2010, Nicolas Niclausse %%% @doc %%% %%% @end %%% Created : 9 Aug 2010 by Nicolas Niclausse <> %%%------------------------------------------------------------------- -module(ts_ip_scan). -behaviour(gen_server). -include("ts_macros.hrl"). %% API -export([start_link/0, get_ip/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(SERVER, ?MODULE). -record(state, {ips}). %%%=================================================================== %%% API %%%=================================================================== get_ip(Interface) -> gen_server:call(?MODULE, {get_ip, Interface}). %%-------------------------------------------------------------------- %% @doc %% Starts the server %% %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} %% @end %%-------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %%-------------------------------------------------------------------- %% @private %% @doc %% Initializes the server %% %% @spec init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% @end %%-------------------------------------------------------------------- init([]) -> ?LOG("Starting ~n",?INFO), {ok, #state{}}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling call messages %% %% @spec handle_call(Request, From, State) -> %% {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_call({get_ip, Interface}, _From, State=#state{ips=undefined}) -> [Val|Rest] = get_intf_aliases(Interface), {reply, Val, State#state{ips=Rest ++ [Val]}}; handle_call({get_ip, _}, _From, State=#state{ips=[Val|Rest]}) -> {reply, Val, State#state{ips=Rest ++ [Val]}}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling cast messages %% %% @spec handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling all non call/cast messages %% %% @spec handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @spec terminate(Reason, State) -> void() %% @end %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% @private %% @doc %% Convert process state when code is changed %% %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} %% @end %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== %% get_intf_aliases(Interface) -> case file:read_file_info("/sbin/ip") of {ok,_} -> Res=os:cmd("LC_ALL=C /sbin/ip -o -f inet addr show dev "++Interface), get_ip_aliases(string:tokens(Res,"\n"), []); {error,Reason} -> ?LOGF("ip command not found (~p), using ifconfig instead~n",[Reason],?NOTICE), Res=os:cmd("LC_ALL=C /sbin/ifconfig "), get_intf_aliases(string:tokens(Res,"\n"), Interface,[],[]) end. get_ip_aliases([], Res) -> Res; get_ip_aliases([Line|Tail], Res) -> [_,_,_,Net|_] =string:tokens(Line," "), [TmpIP|_] =string:tokens(Net,"/"), ?LOGF("found IP: ~p~n",[TmpIP],?DEB), {ok, IP } = inet:getaddr(TmpIP,inet), get_ip_aliases(Tail, [IP|Res]). get_intf_aliases([], _, _, Res) -> Res; get_intf_aliases([" inet addr:"++Line|Tail], Interface, Interface, Res) -> [TmpIP|_] =string:tokens(Line," "), ?LOGF("found IP: ~p~n",[TmpIP],?DEB), {ok, IP } = inet:getaddr(TmpIP,inet), get_intf_aliases(Tail, Interface, Interface, lists:append([IP],Res)); get_intf_aliases([" "++_Line|Tail], Interface, Current, Res) -> get_intf_aliases(Tail, Interface, Current, Res); get_intf_aliases([" "|Tail], Interface, Old, Res) -> get_intf_aliases(Tail, Interface, Old, Res); get_intf_aliases([Line|Tail], Interface, Old, Res) -> ?LOGF("scan line : ~p~n",[Line],?DEB), %% ?DebugF("scan line : ~p~n",[Line]), case string:str(Line,Interface) of 1 -> [Current|_] =string:tokens(Line," "), ?LOGF("found interface (old is ~p): ~p~n",[Old,Current],?DEB), case string:str(Current, Old++":") of 1 -> % subinterface, don't change current get_intf_aliases(Tail, Interface, Old, Res); _ -> get_intf_aliases(Tail, Interface, Current, Res) end; _ -> get_intf_aliases(Tail, Interface, "", Res) end. tsung-1.8.0/src/tsung/ts_http.erl0000644000201100017670000003373114377756736016525 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two. -module(ts_http). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behavior(ts_plugin). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_http.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, dump/2, parse/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session (ack_type and persistent) %% Returns: {ok, "parse"|"no_ack"|"local", "true"|"false"} %%---------------------------------------------------------------------- session_defaults() -> %% we parse the server response, and continue if the tcp %% connection is closed {ok, true}. %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> UserAgent = ts_session_cache:get_user_agent(), #http{user_agent=UserAgent}. %% @spec decode_buffer(Buffer::binary(),Session::record(http)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#http{chunk_toread = -1, compressed={_,false}}) -> Buffer; decode_buffer(Buffer,#http{chunk_toread = -1, compressed={_,Val}}) -> {Headers, CompressedBody} = split_body(Buffer), Body = decompress(CompressedBody, Val), << Headers/binary, "\r\n\r\n", Body/binary >>; decode_buffer(Buffer,#http{compressed={_,Comp}})-> {Headers, Body} = decode_chunk(Buffer), ?DebugF("body is ~p~n",[Body]), RealBody = decompress(Body, Comp), ?DebugF("decoded buffer: ~p",[RealBody]), <>. %% @spec dump(protocol, {Request::ts_request(),Session::term(), Id::integer(), %% Host::string(),DataSize::integer()}) -> ok %% @doc log request and response summary %% @end dump(protocol, Args)-> Data = dump2str(Args), ts_mon_cache:dump({protocol, self(), Data }); dump(protocol_local, Args)-> Data = dump2str(Args), ?DebugF("local protocol: ~p",[Data]), ts_local_mon:dump({protocol, self(), Data }); dump(_,_) -> ok. dump2str({#ts_request{param=HttpReq},HttpResp,UserId,Server,Size,Duration,Transactions})-> Status = case element(2,HttpResp#http.status) of none -> "error_no_http_status"; % something is really wrong here ... http 0.9 response ? Int when is_integer(Int) -> integer_to_list(Int) end, Match = case erase(last_match) of undefined -> ""; {count, Val} -> atom_to_list(Val) end, Error = case erase(protocol_error) of undefined -> ""; Err -> atom_to_list(Err) end, Tr=ts_utils:log_transaction(Transactions), ts_utils:join(";",[integer_to_list(UserId), atom_to_list(HttpReq#http_request.method), Server, get(last_url), Status,integer_to_list(Size), Duration,Tr,Match,Error, HttpReq#http_request.tag] ). %%---------------------------------------------------------------------- %% Function: get_message/21 %% Purpose: Build a message/request , %% Args: #http_request %% Returns: binary %%---------------------------------------------------------------------- get_message(Req=#http_request{url=URL},#state_rcv{session=S}) -> put(last_url,URL), {get_message2(Req),S}. get_message2(Req=#http_request{method=get}) -> ts_http_common:http_no_body(?GET, Req); get_message2(Req=#http_request{method=head}) -> ts_http_common:http_no_body(?HEAD, Req); get_message2(Req=#http_request{method=delete}) -> ts_http_common:http_body(?DELETE, Req); get_message2(Req=#http_request{method=post}) -> ts_http_common:http_body(?POST, Req); get_message2(Req=#http_request{method=options}) -> ts_http_common:http_no_body(?OPTIONS, Req); get_message2(Req=#http_request{method=put}) -> ts_http_common:http_body(?PUT, Req); get_message2(Req=#http_request{method=patch}) -> ts_http_common:http_body(?PATCH, Req); get_message2(Req=#http_request{method=purge}) -> ts_http_common:http_body(?PURGE, Req). %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: Parse the given data and return a new state %% Args: Data (binary) %% State (record) %% Returns: {NewState, Options for socket (list), Close} %%---------------------------------------------------------------------- parse(Data, State) -> ts_http_common:parse(Data, State). parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data, State). %%---------------------------------------------------------------------- %% Function: parse_config/2 %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_http:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: add dynamic parameters to build the message %% this is used for ex. for Cookies in HTTP %% Args: Subst (true|false), {DynVars = #dynvars, #http_session}, Param = #http_request, %% HostData = {Hostname, Port} %% Returns: #http_request or { #http_request, {Host, Port, Scheme}} %%---------------------------------------------------------------------- add_dynparams(false, {_DynVars, Session}, Param, HostData) -> add_dynparams(Session, Param, HostData); add_dynparams(SubstParam, {DynVars, Session}, OldReq=#http_request{url=OldUrl}, HostData={_PrevHost, _PrevPort, PrevProto}) -> Req = subst(SubstParam, OldReq, DynVars), case Req#http_request.url of OldUrl -> add_dynparams(Session,Req, HostData); "http" ++ Rest -> % URL has changed and is absolute URL=ts_config_http:parse_URL(Req#http_request.url), ?LOGF("URL dynamic subst: ~p~n",[URL],?INFO), NewPort = ts_config_http:set_port(URL), NewReq = add_dynparams(Session, Req#http_request{host_header=undefined}, {URL#url.host, NewPort, PrevProto, URL#url.scheme}), % add scheme case OldReq#http_request.use_proxy of true -> NewReq#http_request{url="http"++Rest}; _ -> NewUrl=ts_config_http:set_query(URL), {NewReq#http_request{url=NewUrl}, {URL#url.host, NewPort,ts_config_http:set_scheme({URL#url.scheme,PrevProto})}} end; _ -> % Same host:port add_dynparams(Session, Req, HostData) end. %% Function: add_dynparams/3 add_dynparams(Session,Param=#http_request{host_header=undefined}, HostData )-> Header = case HostData of {Host,80, _,http}-> Host; {Host,443,_,https}-> Host; {Host,80, ts_tcp}-> Host; {Host,443, ts_ssl}-> Host; {Host,80, ts_tcp6}-> ts_config_http:encode_ipv6_address(Host); {Host,443, ts_ssl6}-> ts_config_http:encode_ipv6_address(Host); {Host,Port,_,_} -> ts_config_http:encode_ipv6_address(Host)++":"++ integer_to_list(Port); {Host,Port,_Proto} -> ts_config_http:encode_ipv6_address(Host)++":"++ integer_to_list(Port) end, ?DebugF("set host header dynamically: ~s~n",[Header]), add_dynparams(Session, Param#http_request{host_header=Header},HostData); %% no cookies add_dynparams(#http{session_cookies=[],user_agent=UA},Param, _) -> Param#http_request{user_agent=UA}; %% cookies add_dynparams(#http{session_cookies=DynCookie,user_agent=UA}, Req, _) -> %% FIXME: should we use the Port value in the Cookie ? Cookie=DynCookie++Req#http_request.cookie, Req#http_request{cookie=Cookie,user_agent=UA}. %%---------------------------------------------------------------------- %% @spec subst(SubstParam::true|all_except_body, Req::#http_request{}, %% DynData::#dynvars{} ) -> #http_request{} %% @doc Replace on the fly dynamic element of the HTTP request For %% the moment, we only do dynamic substitution in URL, body, %% userid, passwd, because we see no need for the other HTTP %% request parameters. %% @end %%---------------------------------------------------------------------- subst(SubstParam, Req=#http_request{url=URL, body=Body, headers = Headers, oauth_url=OUrl, oauth_access_token=AToken, oauth_access_secret=ASecret,digest_qop = QOP, digest_cnonce=CNonce, digest_nc=Nc,digest_nonce=Nonce, digest_opaque=Opaque, realm=Realm, userid=UserId, passwd=Passwd, cookie = Cookies, content_type=ContentType}, DynVars) -> Req#http_request{url = escape_url(ts_search:subst(URL, DynVars)), body = case SubstParam of true -> ts_search:subst(Body, DynVars); all_except_body -> Body end, headers = lists:foldl(fun ({Name, Value}, Result) -> [{Name, ts_search:subst(Value, DynVars)} | Result] end, [], Headers), oauth_access_token = ts_search:subst(AToken, DynVars), digest_nonce = ts_search:subst(Nonce, DynVars), digest_cnonce = ts_search:subst(CNonce, DynVars), digest_nc = ts_search:subst(Nc, DynVars), digest_opaque = ts_search:subst(Opaque, DynVars), digest_qop = ts_search:subst(QOP, DynVars), realm = ts_search:subst(Realm, DynVars), content_type = ts_search:subst(ContentType, DynVars), oauth_access_secret = ts_search:subst(ASecret, DynVars), oauth_url = ts_search:subst(OUrl, DynVars), cookie = lists:foldl( fun (#cookie{ value = Value } = C, Result) -> [C#cookie{ value = ts_search:subst(Value, DynVars) } | Result] end, [], Cookies), userid = ts_search:subst(UserId, DynVars), passwd = ts_search:subst(Passwd, DynVars)}. %% URL substitution, we must escape some characters %% currently, we only handle space conversion to %20 escape_url(URL)-> re:replace(URL," ","%20",[{return,list},global]). decompress(Buffer,gzip)-> zlib:gunzip(Buffer); decompress(Buffer,uncompress)-> zlib:uncompress(Buffer); decompress(Buffer,deflate)-> zlib:unzip(Buffer); decompress(Buffer,false)-> Buffer; decompress(Buffer,Else)-> ?LOGF("Unknown compression method, skip decompression ~p",[Else],?WARN), Buffer. decode_chunk(Data)-> decode_chunk_header(Data,<<>>). decode_chunk_header(<>,Headers) when CRLF == << "\r\n\r\n">> -> decode_chunk_size(Data,Headers,<< >>, << >>); decode_chunk_header(<>, Head) -> decode_chunk_header(Data, <> ). decode_chunk_size(<< >>, Headers, Body, _Digits) -> {Headers, Body}; decode_chunk_size(<>, Headers, Body, <<>>) when Head == << "\r\n" >> -> %last CRLF, remove {Headers, Body}; decode_chunk_size(<>, Headers, Body, <<>>) when Head == << "\r\n" >> -> % CRLF but no digits, end of chunk ?Debug("decode chunk: crlf, no digit"), decode_chunk_size(Data, Headers, Body, <<>>); decode_chunk_size(<>, Headers, Body,Digits) when Head == << "\r\n" >> -> case httpd_util:hexlist_to_integer(binary_to_list(Digits)) of 0 -> decode_chunk_size(Data, Headers, Body ,<<>>); Size -> ?DebugF("decode chunk size ~p~n",[Size]), << Chunk:Size/binary, Tail/binary >> = Data, decode_chunk_size(Tail, Headers, << Body/binary, Chunk/binary>> ,<<>>) end; decode_chunk_size(<>, Headers, Body, PrevDigit) -> ?DebugF("chunk one digit ~p~n",[Digit]), decode_chunk_size(Data, Headers, Body, <>). split_body(Data) -> case re:run(Data,"(.*?)\r\n\r\n(.*)",[{capture,all_but_first,binary},dotall]) of nomatch -> Data; {match, [Header,Body]} -> {Header, Body}; _ -> Data end. tsung-1.8.0/src/tsung/ts_http_common.erl0000644000201100017670000007723014377756736020077 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2004 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% common functions used by http clients to: %%% - set HTTP requests %%% - parse HTTP response from server -module(ts_http_common). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -include("ts_profile.hrl"). -include("ts_http.hrl"). -include("ts_config.hrl"). -export([ http_get/1, http_post/1, http_body/2, http_no_body/2, parse/2, parse_req/1, parse_req/2, get_line/1 ]). %%---------------------------------------------------------------------- %% Func: http_get/1 %%---------------------------------------------------------------------- http_get(Args) -> http_no_body(?GET, Args). %%---------------------------------------------------------------------- %% Func: http_get/1 %% Args: #http_request %%---------------------------------------------------------------------- %% normal request http_no_body(Method,#http_request{url=URL, version=Version, cookie=Cookie, headers=Headers, user_agent=UA, get_ims_date=undefined, soap_action=SOAPAction, host_header=Host}=Req)-> ?DebugF("~p ~p~n",[Method,URL]), R = list_to_binary([Method, " ", URL," ", "HTTP/", Version, ?CRLF, set_header("Host",Host,Headers, ""), set_header("User-Agent",UA,Headers, ?USER_AGENT), set_header("Content-Type", undefined, Headers, undefined), set_header("Content-Length", undefined, Headers, undefined), authenticate(Req), oauth_sign(Method,Req), soap_action(SOAPAction), set_cookie_header({Cookie, Host, URL}), headers(Headers), ?CRLF]), ?DebugF("Headers~n-------------~n~s~n",[R]), R; %% if modified since request http_no_body(Method,#http_request{url=URL, version=Version, cookie=Cookie, headers=Headers, user_agent=UA, get_ims_date=Date, soap_action=SOAPAction, host_header=Host}=Req) -> ?DebugF("~p ~p~n",[Method, URL]), list_to_binary([Method, " ", URL," ", "HTTP/", Version, ?CRLF, ["If-Modified-Since: ", Date, ?CRLF], set_header("Host",Host,Headers, ""), set_header("User-Agent",UA,Headers, ?USER_AGENT), set_header("Content-Type", undefined, Headers, undefined), set_header("Content-Length", undefined, Headers, undefined), soap_action(SOAPAction), authenticate(Req), oauth_sign(Method,Req), set_cookie_header({Cookie, Host, URL}), headers(Headers), ?CRLF]). %%---------------------------------------------------------------------- %% Func: http_post/1 %%---------------------------------------------------------------------- http_post(Args) -> http_body(?POST, Args). %%---------------------------------------------------------------------- %% Func: http_body/2 %% Args: #http_request %%---------------------------------------------------------------------- http_body(Method,#http_request{url=URL, version=Version, cookie=Cookie, headers=Headers, user_agent=UA, soap_action=SOAPAction, content_type=ContentType, body=Content, host_header=Host}=Req) -> ContentLength=integer_to_list(size(Content)), ?DebugF("Content Length of POST: ~p~n.", [ContentLength]), H = [Method, " ", URL," ", "HTTP/", Version, ?CRLF, set_header("Host",Host,Headers, ""), set_header("User-Agent",UA,Headers, ?USER_AGENT), set_header("Content-Type", ContentType, Headers, undefined), set_header("Content-Length", ContentLength, Headers, undefined), authenticate(Req), soap_action(SOAPAction), oauth_sign(Method, Req), set_cookie_header({Cookie, Host, URL}), headers(Headers), ?CRLF ], ?LOGF("Headers~n-------------~n~s~n",[H],?DEB), list_to_binary([H, Content ]). %%---------------------------------------------------------------------- %% some HTTP headers functions %%---------------------------------------------------------------------- authenticate(#http_request{userid=undefined})-> []; authenticate(#http_request{passwd=undefined})-> []; authenticate(#http_request{passwd=Passwd, auth_type="basic",userid=UserId})-> AuthStr = ts_utils:encode_base64(lists:append([UserId,":",Passwd])), ["Authorization: Basic ",AuthStr,?CRLF]; authenticate(#http_request{method=Method, passwd=Passwd,userid=UserId, auth_type="digest", realm=Realm, digest_cnonce=CNonce, digest_nc=NC, digest_qop=QOP, digest_nonce=Nonce, digest_opaque=Opaque, url=URL }) -> HA1 = md5_hex(string:join([UserId, Realm, Passwd], ":")), HA2 = md5_hex(string:join([string:to_upper(atom_to_list(Method)), URL], ":")), Response = digest_response({HA1, Nonce,NC, CNonce,QOP,HA2}), digest_header(UserId,Realm,Nonce,URL,QOP,NC,CNonce,Response,Opaque). digest_header(User,Realm,Nonce,URI, QOP,NC,CNonce, Response,Opaque) -> Acc= ["Authorization: Digest " "username=\"",User,"\", ", "realm=\"", Realm, "\", ", "nonce=\"", Nonce, "\", ", "uri=\"", URI, "\", ", "response=\"", Response, "\""], digest_header_opt(Acc, QOP, NC, CNonce, Opaque). %% qop and opaque are undefined digest_header_opt(Acc, undefined, _NC, _CNonce, undefined) -> [Acc, ?CRLF]; digest_header_opt(Acc, QOP, NC, CNonce, Opaque) when is_list(Opaque)-> NewAcc=[Acc,", opaque=\"",Opaque,"\""], digest_header_opt(NewAcc,QOP,NC,CNonce,undefined); digest_header_opt(Acc, QOP, NC, CNonce,undefined) -> NewAcc=[Acc,", qop=\"",QOP,"\"", ", nc=", NC, ", cnonce=\"", CNonce, "\"" ], digest_header_opt(NewAcc,undefined,"","",undefined). digest_response({HA1,Nonce, _NC, _CNonce, undefined, HA2})-> %qop undefined md5_hex(string:join([HA1, Nonce, HA2], ":")); digest_response({HA1,Nonce, NC, CNonce, QOP, HA2})-> md5_hex(string:join([HA1,Nonce,NC,CNonce,QOP,HA2], ":")). md5_hex(String)-> lists:flatten([io_lib:format("~2.16.0b",[N])||N<-binary_to_list(erlang:md5(String))]). oauth_sign(_, #http_request{oauth_consumer = undefined})->[]; oauth_sign(Method, #http_request{url=URL, oauth_consumer=Consumer, oauth_access_token=AccessToken, oauth_access_secret=AccessSecret, oauth_url=ServerURL, content_type = ContentType, body = Body})-> %%UrlParams = oauth_uri:params_from_string(URL), [_He|Ta] = string:tokens(URL,"?"), UrlParams = oauth_uri:params_from_string(lists:flatten(Ta)), AllParams = case ContentType of ?BODY_PARAM -> BodyParams = oauth_uri:params_from_string(lists:flatten(binary_to_list(Body))), UrlParams ++ BodyParams; _ -> UrlParams end, Params = oauth:signed_params(Method, ServerURL, AllParams, Consumer, AccessToken, AccessSecret), ["Authorization: OAuth ", oauth_uri:params_to_header_string(Params),?CRLF]. %%---------------------------------------------------------------------- %% @spec set_header(Name::string, Val::string | undefined, Headers::List, %% Default::string) -> list() %% @doc If header Name is defined in Headers, print this one, otherwise, %% print the given Value (or the default one if undefined) %% @end %%---------------------------------------------------------------------- set_header(Name, Value, Headers, Default) when length(Headers) > 0 -> case lists:keysearch(string:to_lower(Name), 1, normalize_headers(Headers)) of {value, {_,Val}} -> [Name, ": ", Val, ?CRLF]; false -> set_header(Name,Value,[], Default) end; set_header(_Name, undefined, [], undefined) -> []; set_header(Name, undefined, [], Default) -> [Name++": ", Default, ?CRLF]; set_header(Name, Value, [], _) -> [Name++": ", Value, ?CRLF]. soap_action(undefined) -> []; soap_action(SOAPAction) -> ["SOAPAction: \"", SOAPAction, "\"", ?CRLF]. % user defined headers headers([]) -> []; headers(Headers) -> HeadersToIgnore = ["host", "user-agent", "content-type", "content-length"], lists:foldl(fun({Name, Value}, Result) -> case lists:member(string:to_lower(Name), HeadersToIgnore) of true -> Result; _ -> [Name, ": ", Value, ?CRLF | Result] end end, [], lists:reverse(Headers)). normalize_headers([]) -> []; normalize_headers(Headers) -> lists:map(fun({Name, Value}) -> {string:to_lower(Name), Value} end, Headers). %%---------------------------------------------------------------------- %% Function: set_cookie_header/1 %% Args: Cookies (list), Hostname (string), URL %% Purpose: set Cookie: Header %%---------------------------------------------------------------------- set_cookie_header({[], _, _}) -> []; set_cookie_header({Cookies, Host, URL})-> MatchDomain = fun (A) -> matchdomain_url(A,Host,URL) end, CurCookies = lists:filter(MatchDomain, Cookies), set_cookie_header(CurCookies, Host, []). set_cookie_header([], _Host, []) -> []; set_cookie_header([], _Host, Acc) -> [lists:reverse(Acc), ?CRLF]; set_cookie_header([Cookie|Cookies], Host, []) -> set_cookie_header(Cookies, Host, [["Cookie: ", cookie_rec2str(Cookie)]]); set_cookie_header([Cookie|Cookies], Host, Acc) -> set_cookie_header(Cookies, Host, [["; ", cookie_rec2str(Cookie)]|Acc]). cookie_rec2str(#cookie{key=Key, value=Val}) -> lists:append([Key,"=",Val]). %%---------------------------------------------------------------------- %% Function: matchdomain_url/3 %% Purpose: return a cookie only if domain match %% Returns: true|false %%---------------------------------------------------------------------- matchdomain_url(Cookie, _Host, "http"++URL) -> % absolute URL, a proxy is used. %% FIXME: the domain stored is the domain of the proxy, we can't %% check the domain currently :( We assume it's OK %% FIXME: really check if it's a sub path; currently we only check %% that the path is somewhere in the URL which is obviously not %% the right thing to do. string:str(URL,Cookie#cookie.path) > 0; matchdomain_url(Cookie, Host, URL) -> SubDomain = string:str([$.|Host],Cookie#cookie.domain), SubPath = string:str(URL,Cookie#cookie.path), % FIXME:should use regexp:match case {SubDomain,SubPath} of {0,_} -> false; {_,1} -> true; {_,_} -> false end. %%---------------------------------------------------------------------- %% Func: parse/2 %% Args: Data, State %% Returns: {NewState, Options for socket (list), Close} %% Purpose: parse the response from the server and keep information %% about the response if State#state_rcv.session %%---------------------------------------------------------------------- parse(closed, State=#state_rcv{session=Http}) -> {State#state_rcv{session=reset_session(Http), ack_done = true}, [], true}; parse(Data, State=#state_rcv{session=HTTP}) when element(1,HTTP#http.status) == none; HTTP#http.partial == true -> List = binary_to_list(Data), TotalSize = size(Data), Header = State#state_rcv.acc ++ List, case parse_headers(HTTP, Header, State#state_rcv.host) of %% Partial header: {more, HTTPRec, Tail} -> ?LOGF("Partial Header: [HTTP=~p : Tail=~p]~n",[HTTPRec, Tail],?DEB), {State#state_rcv{ack_done=false,session=HTTPRec,acc=Tail},[],false}; %% Complete header, chunked encoding {ok, Http=#http{content_length=0, chunk_toread=0}, Tail} -> NewCookies = concat_cookies(Http#http.cookie, Http#http.session_cookies), case parse_chunked(Tail, State#state_rcv{session=Http, acc=[]}) of {NewState=#state_rcv{ack_done=false, session=NewHttp}, Opts} -> {NewState#state_rcv{session=NewHttp#http{session_cookies=NewCookies}}, Opts, false}; {NewState=#state_rcv{session=NewHttp}, Opts} -> {NewState#state_rcv{acc=[],session=NewHttp#http{session_cookies=NewCookies}}, Opts, Http#http.close} end; {ok, Http=#http{content_length=0, close=true}, _} -> %% no content length, close=true: the server will close the connection NewCookies = concat_cookies(Http#http.cookie, Http#http.session_cookies), {State#state_rcv{ack_done = false, datasize = TotalSize, session=Http#http{session_cookies=NewCookies}}, [], true}; {ok, Http=#http{status={100,_}}, _} -> % Status 100 Continue, ignore. %% FIXME: not tested {State#state_rcv{ack_done=false,session=reset_session(Http)},[],false}; {ok, Http, Tail} -> NewCookies = concat_cookies(Http#http.cookie, Http#http.session_cookies), check_resp_size(Http#http{session_cookies=NewCookies}, length(Tail), State#state_rcv{acc=[]}, TotalSize, State#state_rcv.dump) end; %% continued chunked transfer parse(Data, State=#state_rcv{session=Http}) when Http#http.chunk_toread >=0 -> ?DebugF("Parse chunk data = [~s]~n", [Data]), case read_chunk_data(Data,State,Http#http.chunk_toread,Http#http.body_size) of {NewState=#state_rcv{ack_done=false}, NewOpts}-> {NewState, NewOpts, false}; {NewState, NewOpts}-> {NewState#state_rcv{acc=[]}, NewOpts, Http#http.close} end; %% continued normal transfer parse(Data, State=#state_rcv{session=Http, datasize=PreviousSize}) -> DataSize = size(Data), ?DebugF("HTTP Body size=~p ~n",[DataSize]), CLength = Http#http.content_length, case Http#http.body_size + DataSize of CLength -> % end of response {State#state_rcv{session=reset_session(Http), acc=[], ack_done = true, datasize = DataSize+PreviousSize}, [], Http#http.close}; Size -> {State#state_rcv{session = Http#http{body_size = Size}, ack_done = false, datasize = DataSize+PreviousSize}, [], false} end. %%---------------------------------------------------------------------- %% Func: check_resp_size/5 %% Purpose: Check response size %% Returns: {NewState= record(state_rcv), SockOpts, Close} %%---------------------------------------------------------------------- check_resp_size(Http=#http{content_length=CLength, close=Close}, CLength, State, DataSize, _Dump) -> %% end of response {State#state_rcv{session= reset_session(Http), ack_done = true, datasize = DataSize }, [], Close}; check_resp_size(Http=#http{content_length=CLength, close=Close}, BodySize, State, DataSize, Dump) when BodySize > CLength -> ?LOGF("Error: HTTP Body (~p)> Content-Length (~p) !~n", [BodySize, CLength], ?ERR), log_error(Dump, error_http_bad_content_length), {State#state_rcv{session= reset_session(Http), ack_done = true, datasize = DataSize }, [], Close}; %% header is complete (partial=false), HTTP Method is HEAD, so we do not %% expect any more data check_resp_size(Http=#http{partial=false, close=Close}, _BodySize, State=#state_rcv{request=#ts_request{param=#http_request{method=head}}}, DataSize, _Dump) -> {State#state_rcv{session=reset_session(Http), ack_done=true, datasize=DataSize}, [], Close}; check_resp_size(Http=#http{}, BodySize, State, DataSize,_Dump) -> %% need to read more data {State#state_rcv{session = Http#http{body_size = BodySize}, ack_done = false, datasize = DataSize },[],false}. %%---------------------------------------------------------------------- %% Func: parse_chunked/2 %% Purpose: parse 'Transfer-Encoding: chunked' for HTTP/1.1 %% Returns: {NewState= record(state_rcv), SockOpts, Close} %%---------------------------------------------------------------------- parse_chunked(Body, State)-> ?DebugF("Parse chunk data = [~s]~n", [Body]), read_chunk(list_to_binary(Body), State, 0, 0). %%---------------------------------------------------------------------- %% Func: read_chunk/4 %% Purpose: the real stuff for parsing chunks is here %% Returns: {NewState= record(state_rcv), SockOpts, Close} %%---------------------------------------------------------------------- read_chunk(<<>>, State, Int, Acc) -> ?LOGF("No data in chunk [Int=~p, Acc=~p] ~n", [Int,Acc],?INFO), AccInt = list_to_binary(httpd_util:integer_to_hexlist(Int)), { State#state_rcv{acc = AccInt, ack_done = false }, [] }; % read more data %% this code has been inspired by inets/http_lib.erl %% Extensions not implemented read_chunk(<>, State=#state_rcv{session=Http}, Int, Acc) -> case Char of <> when $0= read_chunk(Data, State, 16*Int+(C-$0), Acc+1); <> when $a= read_chunk(Data, State, 16*Int+10+(C-$a), Acc+1); <> when $A= read_chunk(Data, State, 16*Int+10+(C-$A), Acc+1); <> when Int>0 -> read_chunk_data(Data, State, Int+3, Acc+1); <> when Int==0, size(Data) == 3 -> %% should be the end of transfer ?DebugF("Finish transfer chunk ~p~n", [binary_to_list(Data)]), {State#state_rcv{session= reset_session(Http), ack_done = true, datasize = Acc %% FIXME: is it the correct size? }, []}; <> when Int==0, size(Data) < 3 -> % lack ?CRLF, continue { State#state_rcv{acc = <<48, ?CR , Data/binary>>, ack_done=false }, [] }; <> when C==$ -> % Some servers (e.g., Apache 1.3.6) throw in % additional whitespace... read_chunk(Data, State, Int, Acc+1); _Other -> ?LOGF("Unexpected error while parsing chunk ~p~n", [_Other] ,?WARN), log_error(State#state_rcv.dump, error_http_unexpected_chunkdata), {State#state_rcv{session= reset_session(Http), ack_done = true}, []} end. %%---------------------------------------------------------------------- %% Func: read_chunk_data/4 %% Purpose: read 'Int' bytes of data %% Returns: {NewState= record(state_rcv), SockOpts} %%---------------------------------------------------------------------- read_chunk_data(Data, State=#state_rcv{acc=[]}, Int, Acc) when size(Data) > Int-> ?DebugF("Read ~p bytes of chunk with size = ~p~n", [Int, size(Data)]), <<_NewData:Int/binary, Rest/binary >> = Data, read_chunk(Rest, State, 0, Int + Acc); read_chunk_data(Data, State=#state_rcv{acc=[],session=Http}, Int, Acc) -> % not enough data in buffer BodySize = size(Data), ?DebugF("Partial chunk received (~p/~p)~n", [BodySize,Int]), NewHttp = Http#http{chunk_toread = Int-BodySize, body_size = BodySize + Acc}, {State#state_rcv{session = NewHttp, ack_done = false, % continue to read data datasize = BodySize + Acc},[]}; read_chunk_data(Data, State=#state_rcv{acc=Acc}, _Int, AccSize) -> ?DebugF("Accumulated data = [~p]~n", [Acc]), NewData = <>, read_chunk(NewData, State#state_rcv{acc=[]}, 0, AccSize). %%---------------------------------------------------------------------- %% Func: add_new_cookie/3 %% Purpose: Separate cookie values from attributes %%---------------------------------------------------------------------- add_new_cookie(Cookie, Host, OldCookies) -> Fields = splitcookie(Cookie), %% FIXME: bad domain if we use a Proxy (the domain will be equal %% to the proxy domain instead of the server's domain New = parse_set_cookie(Fields, #cookie{domain=[$.|Host],path="/"}), concat_cookies([New],OldCookies). %%---------------------------------------------------------------------- %% Function: splitcookie/3 %% Purpose: split according to string ";". %% Not very elegant but 5x faster than the regexp:split version %%---------------------------------------------------------------------- splitcookie(Cookie) -> splitcookie(Cookie, [], []). splitcookie([], Cur, Acc) -> [lists:reverse(Cur)|Acc]; splitcookie(";"++Rest,Cur,Acc) -> splitcookie(string:strip(Rest, both),[],[lists:reverse(Cur)|Acc]); splitcookie([Char|Rest],Cur,Acc)->splitcookie(Rest, [Char|Cur], Acc). %%---------------------------------------------------------------------- %% Func: concat_cookie/2 %% Purpose: add new cookies to a list of old ones. If the keys already %% exists, replace with the new ones %%---------------------------------------------------------------------- concat_cookies([], Cookies) -> Cookies; concat_cookies(Cookie, []) -> Cookie; concat_cookies([New=#cookie{}|Rest], OldCookies)-> case lists:keysearch(New#cookie.key, #cookie.key, OldCookies) of {value, #cookie{domain=Dom}} when Dom == New#cookie.domain -> %same domain ?DebugF("Reset key ~p with new value ~p~n",[New#cookie.key, New#cookie.value]), NewList = lists:keyreplace(New#cookie.key, #cookie.key, OldCookies, New), concat_cookies(Rest, NewList); {value, _Val} -> % same key, but different domains concat_cookies(Rest, [New | OldCookies]); false -> concat_cookies(Rest, [New | OldCookies]) end. %%---------------------------------------------------------------------- %% Func: parse_set_cookie/2 %% cf. RFC 2965 %%---------------------------------------------------------------------- parse_set_cookie([], Cookie) -> Cookie; parse_set_cookie([Field| Rest], Cookie=#cookie{}) -> {Key,Val} = get_cookie_key(Field,[]), ?DebugF("Parse cookie key ~p with value ~p~n",[Key, Val]), parse_set_cookie(Rest, set_cookie_key(Key, Val, Cookie)). %%---------------------------------------------------------------------- set_cookie_key([L|"ersion"],Val,Cookie) when L == $V; L==$v -> Cookie#cookie{version=Val}; set_cookie_key([L|"omain"],Val,Cookie) when L == $D; L==$d -> Cookie#cookie{domain=Val}; set_cookie_key([L|"ath"],Val,Cookie) when L == $P; L==$p -> Cookie#cookie{path=Val}; set_cookie_key([L|"ax-Age"],Val,Cookie) when L == $M; L==$m -> Cookie#cookie{max_age=Val}; % NOT IMPLEMENTED set_cookie_key([L|"xpires"],Val,Cookie) when L == $E; L==$e -> Cookie#cookie{expires=Val}; % NOT IMPLEMENTED set_cookie_key([L|"ort"],Val,Cookie) when L == $P; L==$p -> Cookie#cookie{port=Val}; set_cookie_key([L|"iscard"],_Val,Cookie) when L == $D; L==$d -> Cookie#cookie{discard=true}; % NOT IMPLEMENTED set_cookie_key([L|"ecure"],_Val,Cookie) when L == $S; L==$s -> Cookie#cookie{secure=true}; % NOT IMPLEMENTED set_cookie_key([L|"ommenturl"],_Val,Cookie) when L == $C; L==$c -> Cookie; % don't care about comment set_cookie_key([L|"omment"],_Val,Cookie) when L == $C; L==$c -> Cookie; % don't care about comment set_cookie_key(Key,Val,Cookie) -> Cookie#cookie{key=Key,value=Val}. %%---------------------------------------------------------------------- get_cookie_key([],Acc) -> {lists:reverse(Acc), []}; get_cookie_key([$=|Rest],Acc) -> {lists:reverse(Acc), Rest}; get_cookie_key([Char|Rest],Acc)-> get_cookie_key(Rest, [Char|Acc]). %%-------------------------------------------------------------------- %% Func: parse_headers/3 %% Purpose: Parse HTTP headers line by line %% Returns: {ok, #http, Body} %%-------------------------------------------------------------------- parse_headers(H, Tail, Host) -> case get_line(Tail) of {line, Line, Tail2} -> parse_headers(parse_line(Line, H, Host), Tail2, Host); {lastline, Line, Tail2} -> {ok, parse_line(Line, H#http{partial=false}, Host), Tail2}; {more} -> %% Partial header {more, H#http{partial=true}, Tail} end. %%-------------------------------------------------------------------- %% Func: parse_req/1 %% Purpose: Parse HTTP request %% Returns: {ok, #http_request, Body} | {more, Http , Tail} %%-------------------------------------------------------------------- parse_req(Data) -> parse_req([], Data). parse_req([], Data) -> FunV = fun("http/"++V)->V;("HTTP/"++V)->V end, case get_line(Data) of {more} -> %% Partial header {more, [], Data}; {line, Line, Tail} -> [Method, RequestURI, Version] = string:tokens(Line," "), parse_req(#http_request{method=http_method(Method), url=RequestURI, version=FunV(Version)},Tail); {lastline, Line, Tail} -> [Method, RequestURI, Version] = string:tokens(Line," "), {ok, #http_request{method=http_method(Method), url=RequestURI, version=FunV(Version)},Tail} end; parse_req(Http=#http_request{headers=H}, Data) -> case get_line(Data) of {line, Line, Tail} -> NewH= [ts_utils:split2(Line,$:,strip) | H], parse_req(Http#http_request{headers=NewH}, Tail); {lastline, Line, Tail} -> NewH= [ts_utils:split2(Line,$:,strip) | H], {ok, Http#http_request{headers=NewH}, Tail}; {more} -> %% Partial header {more, Http#http_request{id=partial}, Data} end. %%-------------------------------------------------------------------- http_method("get")-> 'GET'; http_method("post")-> 'POST'; http_method("head")-> 'HEAD'; http_method("put")-> 'PUT'; http_method("delete")-> 'DELETE'; http_method("connect")-> 'CONNECT'; http_method("propfind")-> 'PROPFIND'; http_method("proppatch")-> 'PROPPATCH'; http_method("copy")-> 'COPY'; http_method("move")-> 'MOVE'; http_method("lock")-> 'LOCK'; http_method("unlock")-> 'UNLOCK'; http_method("mkcol")-> 'MKCOL'; http_method("mkactivity")-> 'MKACTIVITY'; http_method("report")-> 'REPORT'; http_method("options")-> 'OPTIONS'; http_method("checkout")-> 'CHECKOUT'; http_method("merge")-> 'MERGE'; http_method("patch")-> 'PATCH'; http_method(Method) -> ?LOGF("Unknown HTTP method: ~p~n", [Method] ,?WARN), not_implemented. %%-------------------------------------------------------------------- %% Func: parse_status/2 %% Purpose: Parse HTTP status %% Returns: #http %%-------------------------------------------------------------------- parse_status([A,B,C|_], Http=#http{status={Prev,_}}) -> Status=list_to_integer([A,B,C]), ?DebugF("HTTP Status ~p~n",[Status]), ts_mon_cache:add({ count, Status }), Http#http{status={Status,Prev}}. %%-------------------------------------------------------------------- %% Func: parse_line/3 %% Purpose: Parse a HTTP header %% Returns: #http %%-------------------------------------------------------------------- parse_line("http/1.1 " ++ TailLine, Http, _Host )-> parse_status(TailLine, Http); parse_line("http/1.0 " ++ TailLine, Http, _Host)-> parse_status(TailLine, Http#http{close=true}); parse_line("content-length: "++Tail, Http, _Host) when hd(Tail) /= $\s -> %% tuning: handle common case (single LWS) to avoid a call to string:strip CL = list_to_integer(Tail), ?DebugF("HTTP Content-Length ~p~n",[CL]), Http#http{content_length=CL}; parse_line("content-length: "++Tail, Http, _Host)-> % multiple white spaces CL = list_to_integer(string:strip(Tail)), ?DebugF("HTTP Content-Length ~p~n",[CL]), Http#http{content_length=CL}; parse_line("connection: close"++_Tail, Http, _Host)-> ?Debug("Connection Closed in Header ~n"), Http#http{close=true}; parse_line("content-encoding: "++Tail, Http=#http{compressed={Prev,_}}, _Host)-> ?DebugF("content encoding:~p ~n",[Tail]), Http#http{compressed={list_to_atom(Tail),Prev}}; parse_line("transfer-encoding:"++Tail, Http, _Host)-> ?DebugF("~p transfer encoding~n",[Tail]), case string:strip(Tail) of [C|"hunked"++_] when C == $C; C == $c -> Http#http{chunk_toread=0}; _ -> ?LOGF("Unknown transfer encoding ~p~n",[Tail],?NOTICE), Http end; parse_line("set-cookie: "++Tail, Http=#http{cookie=PrevCookies}, Host)-> Cookie = add_new_cookie(Tail, Host, PrevCookies), ?DebugF("HTTP New cookie val ~p~n",[Cookie]), Http#http{cookie=Cookie}; parse_line("proxy-connection: keep-alive"++_Tail, Http, _Host)-> Http#http{close=false}; parse_line("connection: Keep-Alive"++_Tail, Http, _Host)-> Http#http{close=false}; parse_line(_Line, Http, _Host) -> ?DebugF("Skip header ~p (Http record is ~p)~n", [_Line, Http]), Http. %% code taken from yaws is_nb_space(X) -> lists:member(X, [$\s, $\t]). % ret: {line, Line, Trail} | {lastline, Line, Trail} get_line(L) -> get_line(L, true, []). get_line("\r\n\r\n" ++ Tail, _Cap, Cur) -> {lastline, lists:reverse(Cur), Tail}; get_line("\r\n", _, _) -> {more}; get_line("\r\n" ++ Tail, Cap, Cur) -> case is_nb_space(hd(Tail)) of true -> %% multiline ... continue get_line(Tail, Cap,[$\n, $\r | Cur]); false -> {line, lists:reverse(Cur), Tail} end; get_line([$:|T], true, Cur) -> % ':' separator get_line(T, false, [$:|Cur]);%the rest of the header isn't set to lower char get_line([H|T], false, Cur) -> get_line(T, false, [H|Cur]); get_line([Char|T], true, Cur) when Char >= $A, Char =< $Z -> get_line(T, true, [Char + 32|Cur]); get_line([H|T], true, Cur) -> get_line(T, true, [H|Cur]); get_line([], _, _) -> %% Headers are fragmented ... We need more data {more}. %% we need to keep the compressed value of the current request reset_session(#http{user_agent=UA,session_cookies=Cookies, compressed={Compressed,_}, status= {Status,_}, chunk_toread=Val}) when Val > -1 -> #http{session_cookies=Cookies,user_agent=UA,compressed={false,Compressed}, chunk_toread=-2, status={none,Status}} ; reset_session(#http{user_agent=UA,session_cookies=Cookies, compressed={Compressed,_}, status= {Status,_}}) -> #http{session_cookies=Cookies,user_agent=UA,compressed={false,Compressed}, status={none,Status}}. log_error(protocol,Error) -> put(protocol_error,Error), log_error2(protocol,Error); log_error(Type,Error) -> log_error2(Type,Error). log_error2(_,Error)-> ts_mon_cache:add({count, Error}). tsung-1.8.0/src/tsung/ts_fs.erl0000644000201100017670000002716214377756736016157 0ustar nniclausdream%%% %%% Copyright 2009 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 20 août 2009 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_fs). -vc('$Id: ts_erlang.erl,v 0.0 2009/08/20 16:31:58 nniclaus Exp $ '). -author('nniclaus@sophia.inria.fr'). -behavior(ts_plugin). -include("ts_macros.hrl"). -include("ts_profile.hrl"). -include("ts_fs.hrl"). -include_lib("kernel/include/file.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, dump/2, parse/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). %%==================================================================== %% Data Types %%==================================================================== %% @type dyndata() = #dyndata{proto=ProtoData::term(),dynvars=list()}. %% Dynamic data structure %% @end %% @type server() = {Host::tuple(),Port::integer(),Protocol::atom()}. %% Host/Port/Protocol tuple %% @end %% @type param() = {dyndata(), server()}. %% Dynamic data structure %% @end %% @type hostdata() = {Host::tuple(),Port::integer()}. %% Host/Port pair %% @end %% @type client_data() = binary() | closed. %% Data passed to a protocol implementation is either a binary or the %% atom closed indicating that the server closed the tcp connection. %% @end %%==================================================================== %% API %%==================================================================== parse_config(El,Config) -> ts_config_fs:parse_config(El, Config). %% @spec session_defaults() -> {ok, Persistent} | {ok, Persistent, Bidi} %% Persistent = bool() %% Bidi = bool() %% @doc Default parameters for sessions of this protocol. Persistent %% is true if connections are preserved after the underlying tcp %% connection closes. Bidi should be true for bidirectional protocols %% where the protocol module needs to reply to data sent from the %% server. @end session_defaults() -> {ok, true}. % not relevant for erlang type (?). %% @spec new_session() -> State::term() %% @doc Initialises the state for a new protocol session. %% @end new_session() -> #fs_session{}. %% @spec decode_buffer(Buffer::binary(),Session::record(fs)) -> NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#fs_session{}) -> Buffer. %% @spec add_dynparams(Subst, dyndata(), param(), hostdata()) -> {dyndata(), server()} | dyndata() %% Subst = term() %% @doc Updates the dynamic request data structure created by %% {@link ts_protocol:init_dynparams/0. init_dynparams/0}. %% @end add_dynparams(false, {_,Session}, Param, HostData) -> add_dynparams(Session, Param, HostData); add_dynparams(true, {DynVars,Session}, Param, HostData) -> NewParam = subst(Param, DynVars), add_dynparams(Session,NewParam, HostData). add_dynparams(#fs_session{position=Pos,iodev=IODevice}, Req=#fs{}, _HostData) when is_integer(Pos)-> Req#fs{position=Pos,iodev=IODevice}; add_dynparams(#fs_session{}, Param, _HostData) -> Param. %%---------------------------------------------------------------------- %% @spec subst(record(fs), dynvars:term()) -> record(fs) %% @doc Replace on the fly dynamic element of the request. %% @end %%---------------------------------------------------------------------- subst(Req=#fs{path=Path,size=Size,dest=Dest}, DynVars) -> Req#fs{path=ts_search:subst(Path,DynVars),dest=ts_search:subst(Dest,DynVars), size=ts_search:subst(Size,DynVars)}. %% @spec parse(Data::client_data(), State) -> {NewState, Opts, Close} %% State = #state_rcv{} %% Opts = proplist() %% Close = bool() %% @doc %% Opts is a list of inet:setopts socket options. Don't change the %% active/passive mode here as tsung will set {active,once} before %% your options. %% Setting Close to true will cause tsung to close the connection to %% the server. %% @end parse({file, open, _Args, {ok,IODevice}},State=#state_rcv{session=S}) -> NewDyn=S#fs_session{iodev=IODevice,position=0}, {State#state_rcv{ack_done=true,datasize=0,session=NewDyn}, [], false}; parse({file, open, [Path,_], {error,Reason}},State) -> ?LOGF("error while opening file: ~p(~p)~n",[Path, Reason],?ERR), ts_mon_cache:add({count,error_fs_open}), {State#state_rcv{ack_done=true,datasize=0}, [], false}; parse({file, close, [_IODevice], ok},State=#state_rcv{session=S}) -> NewDyn=S#fs_session{iodev=undefined,position=0}, {State#state_rcv{ack_done=true,datasize=0,session=NewDyn}, [], false}; parse({file, close, [_IODevice], {error,Reason}}, State) -> ?LOGF("error while closing file: ~p~n",[Reason],?ERR), ts_mon_cache:add({count,error_fs_close}), {State#state_rcv{ack_done=true,datasize=0}, [], false}; parse({file, pread, [_IODev,Pos,Size], {ok,_Data}},State=#state_rcv{session=S,datasize=DataSize}) -> NewDyn=S#fs_session{position=Pos+Size}, {State#state_rcv{ack_done=true,datasize=DataSize+Size,session=NewDyn}, [], false}; parse({file, pread, [_IODev,_Pos,Size], eof},State=#state_rcv{session=S,datasize=DataSize}) -> NewDyn=S#fs_session{position=0}, {State#state_rcv{ack_done=true,datasize=DataSize+Size,session=NewDyn}, [], false}; parse({file, pread, [_IODev,_Pos,_Size], {error,Reason}},State) -> ?LOGF("error while reading file: ~p~n",[Reason],?ERR), ts_mon_cache:add({count,error_fs_pread}), {State#state_rcv{ack_done=true,datasize=0}, [], false}; parse({file, write_file, _Args, ok},State) -> {State#state_rcv{ack_done=true,datasize=0}, [], false}; parse({file, write_file, [Path,_], {error,Reason}},State) -> ?LOGF("error while writing file: ~p (~p)~n",[Path, Reason],?ERR), ts_mon_cache:add({count,error_fs_write}), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, pwrite, [_IODev,Pos,Data], ok},State=#state_rcv{session=S}) -> NewDyn=S#fs_session{position=Pos+length(Data)}, {State#state_rcv{ack_done=true,datasize=0,session=NewDyn}, [], false}; parse({file, pwrite, Args, {error,Reason}},State) -> ?LOGF("error while writing file: ~p (~p)~n",[Args, Reason],?ERR), ts_mon_cache:add({count,error_fs_pwrite}), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, del_dir, [_Path], ok},State) -> {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, del_dir, [Path], {error,Reason}},State) -> ?LOGF("error while delete directory: ~p (~p)~n",[Path, Reason],?ERR), ts_mon_cache:add({count,error_fs_del_dir}), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, make_dir, [_Path], ok},State) -> {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, make_dir, [Path], {error, eexist} },State) -> ?LOGF("error while creating directory: ~p already exists~n",[Path],?NOTICE), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, make_dir, [Path], {error,Reason}},State) -> ?LOGF("error while creating directory: ~p (~p)~n",[Path, Reason],?ERR), ts_mon_cache:add({count,error_fs_mkdir}), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, make_symlink, _Args, ok},State) -> {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, make_symlink, [_Existing, New], {error, eexist} },State) -> ?LOGF("error while creating symlink: ~p already exists~n",[New],?NOTICE), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, make_symlink, [Existing, New], {error,Reason}},State) -> ?LOGF("error while creating symlink: ~p to ~p (~p)~n",[Existing, New, Reason],?ERR), ts_mon_cache:add({count,error_fs_mksymlink}), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, delete, [_Path], ok},State) -> {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, delete, [Path], {error,Reason}},State) -> ?LOGF("error while deleting file: ~p (~p)~n",[Path, Reason],?ERR), {State#state_rcv{ack_done=true, datasize=0}, [], false}; parse({file, read_file_info, [_Path], {ok, _FileInfo}},State) -> %% which value should we use for datasize ? {State#state_rcv{ack_done=true,datasize=0}, [], false}; parse({file, read_file_info, [Path], {error,Reason}},State) -> ?LOGF("error while running stat file: ~p (~p)~n",[Path,Reason],?ERR), ts_mon_cache:add({count,error_fs_stat}), {State#state_rcv{ack_done=true,datasize=0}, [], false}; parse({ts_utils, read_file_raw, [_Path], {ok,_Res,Size}},State) -> {State#state_rcv{ack_done=true,datasize=Size}, [], false}; parse({ts_utils, read_file_raw, [Path], {error,Reason}},State) -> ?LOGF("error while reading file: ~p(~p)~n",[Path,Reason],?ERR), ts_mon_cache:add({count,error_fs_read}), {State#state_rcv{ack_done=true,datasize=0}, [], false}. %% @spec parse_bidi(Data, State) -> {nodata, NewState} | {Data, NewState} %% Data = client_data() %% NewState = term() %% State = term() %% @doc Parse a block of data from the server. No reply will be sent %% if the return value is nodata, otherwise the Data binary will be %% sent back to the server immediately. %% @end parse_bidi(Data, State) -> ts_plugin:parse_bidi(Data,State). dump(A,B) -> ts_plugin:dump(A,B). %% @spec get_message(record(),record(state_rcv)) -> {term(),record(state_rcv)} %% @doc Creates a new message to send to the connected server. %% @end get_message(R,#state_rcv{session=S}) -> {get_message2(R),S}. get_message2(#fs{command=read, path=Path}) -> {ts_utils,read_file_raw,[Path],0}; get_message2(#fs{command=read_chunk, iodev=IODevice,position=Loc, size=Size}) when is_integer(Loc)-> {file,pread,[IODevice,Loc,Size],0}; get_message2(#fs{command=write_chunk, iodev=IODevice,position=Loc, size=Size}) when is_integer(Loc)-> {file,pwrite,[IODevice,Loc,ts_utils:urandomstr(Size)],Size}; get_message2(#fs{command=open, mode=read,path=Path,position=Loc}) when is_integer(Loc)-> {file,open,[Path,[read,raw,binary]],0}; get_message2(#fs{command=open, mode=write,path=Path,position=Loc}) when is_integer(Loc)-> {file,open,[Path,[write,raw,binary]],0}; get_message2(#fs{command=close, iodev=IODevice}) -> {file,close,[IODevice],0}; get_message2(#fs{command=delete, path=Path}) -> {file,delete,[Path],0}; get_message2(#fs{command=del_dir, path=Path}) -> {file,del_dir,[Path],0}; get_message2(#fs{command=make_dir, path=Path}) -> {file,make_dir,[Path],0}; get_message2(#fs{command=make_symlink, path=Existing, dest=New}) -> {file,make_symlink,[Existing, New],0}; get_message2(#fs{command=stat, path=Path}) -> {file,read_file_info,[Path],0}; get_message2(#fs{command=write,path=Path, size=Size}) -> {file,write_file,[Path,ts_utils:urandomstr(Size),[raw]],Size}. tsung-1.8.0/src/tsung/ts_erlang.erl0000644000201100017670000000447714377756736017023 0ustar nniclausdream%%% %%% Copyright 2009 © INRIA %%% %%% Author : Nicolas Niclausse %%% Created: 20 août 2009 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_erlang). -vc('$Id: ts_erlang.erl,v 0.0 2009/08/20 16:31:58 nniclaus Exp $ '). -author('nniclaus@sophia.inria.fr'). -behaviour(gen_ts_transport). -define(TIMEOUT,36000000). % 1 hour -include("ts_profile.hrl"). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2, client/4]). client(MasterPid,Server,Port,Opts)-> receive {Module, Fun, Args, _Size} -> Res=apply(Module,Fun,Args), MasterPid ! {erlang,self(),{Module,Fun,Args,Res}}, client(MasterPid,Server,Port,Opts) after ?TIMEOUT -> MasterPid ! timeout end. protocol_options(_Opts) -> []. %% -> {ok, Socket} connect(Host, Port, Opts, _Timeout) -> Pid=spawn_link(ts_erlang,client,[self(),Host,Port,Opts]), {ok, Pid}. %% send/3 -> ok | {error, Reason} send(Pid, Data, _Opts) -> Pid ! Data, ok. close(_Socket) -> ok. set_opts(Socket, _Opts) -> Socket. normalize_incomming_data(_Socket, Data={timeout,_,_}) -> Data; normalize_incomming_data(Socket, Data) -> {gen_ts_transport, Socket, Data}. tsung-1.8.0/src/tsung/ts_dynvars.erl0000644000201100017670000001135414377756736017231 0ustar nniclausdream%%% Copyright (C) 2008 Pablo Polvorin %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. %%% @copyright (C) 2008 Pablo Polvorin %%% @author Pablo Polvorin %%% @author Nicolas Niclausse %%% @doc functions to manipulate dynamic variables, sort of Abstract Data Type %%% @end %%% created on 2008-08-22 %%% modified by Nicolas Niclausse: Add merge and multi keys/values (new, set) -module(ts_dynvars). -export([new/0,new/2, merge/2, lookup/2,lookup/3,set/3,entries/1,map/4]). -define(IS_DYNVARS(X),is_list(X)). %% @type dynvar() = {Key::atom(), Value::string()} | []. %% @type dynvars() = [dynvar()] %% @spec new() -> [dynvar()] new() -> []. new(Key, Val) when is_atom(Key)-> [{Key,Val}]; new(VarNames, Values) when is_list(VarNames),is_list(Values)-> %% FIXME: check if VarNames is a list of atoms case {length(VarNames), length(Values)} of {A,A} -> lists:zip(VarNames,Values); {A,B} when A > B -> % more names than values, use empty values lists:zip(VarNames,Values ++ lists:duplicate(A-B,"")); {C,D} when C < D -> % more values than names, remove unused values lists:zip(VarNames,lists:sublist(Values, C)) end. %% @spec lookup(Key::atom(), Dynvar::dynvars()) -> {ok,Value::term()} | false lookup(_Key, []) -> false; lookup(Key, DynVars) when ?IS_DYNVARS(DynVars), is_atom(Key)-> case lists:keysearch(Key,1,DynVars) of {value,{Key,Value}} -> {ok,Value}; false -> false end; lookup({Key, Index}, DynVars) when ?IS_DYNVARS(DynVars), is_atom(Key), is_integer(Index)-> case lists:keysearch(Key,1,DynVars) of {value,{Key,Value}} -> {ok,lists:nth(Index,Value)}; false -> false end. %% @doc same as lookup/2, only that if the key isn't present, the default %% value is returned instead of returning false. lookup(Key, DynVars, Default) when ?IS_DYNVARS(DynVars), is_atom(Key)-> case lookup(Key, DynVars) of false -> {ok,Default}; R -> R end. %% @spec set(Key::atom(), Value::term(), DynVars::dynvars()) -> dynvars() set(Key,Value,DynVars) when ?IS_DYNVARS(DynVars),is_atom(Key) -> merge([{Key, Value}],DynVars); %% optimization: only one key and one value set([Key],[Value],DynVars) -> set(Key,Value,DynVars); set([Key],Value,DynVars) -> %% for backward compatibility set(Key,Value,DynVars); %% general case: list of keys and values set(Keys,Values,DynVars) when ?IS_DYNVARS(DynVars),is_list(Keys),is_list(Values) -> merge(new(Keys,Values),DynVars). entries(DynVars) when ?IS_DYNVARS(DynVars) -> DynVars. %% @spec map(Fun::function(),Key::atom(),Default::term(),DynVars::dynvars()) %% -> dynvars() %% @doc The value associated to key Key is replaced with %% the result of applying function Fun to its previous value. %% If there is no such previous value, Fun is applied to the default %% value Default. %% map(fun(I) -> I +1 end,b,0,[{a,5}]) => [{a,5},{b,1}] %% map(fun(I) -> I +1 end,b,0,[{a,1}]) => [{a,5},{b,2}] map(Fun,Key,Default,DynVars) when ?IS_DYNVARS(DynVars),is_atom(Key),is_function(Fun,1) -> do_map(Fun,Key,Default,DynVars,[]). do_map(Fun,Key,Default,[],Acc) -> [{Key,Fun(Default)}| Acc]; do_map(Fun,Key,_Default,[{Key,Value}|Rest],Acc) -> lists:append([[{Key,Fun(Value)}], Rest, Acc]); do_map(Fun,Key,Default,[H|Rest], Acc) -> do_map(Fun,Key,Default,Rest, [H|Acc]). %% @spec merge(DynVars::dynvars(),DynVars::dynvars()) -> dynvars() %% @doc merge two set of dynamic variables merge(DynVars1, DynVars2) when ?IS_DYNVARS(DynVars1),?IS_DYNVARS(DynVars2) -> ts_utils:keyumerge(1,DynVars1,DynVars2); merge(_, DynVars2) -> DynVars2. tsung-1.8.0/src/tsung/ts_digest.erl0000644000201100017670000000667614377756736017035 0ustar nniclausdream%%% %%% Created: Apr 2006 by Jason Tucker %%% %%% Modified by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_digest). -author('jasonwtucker@gmail.com'). -export([ digest/2, sip_digest/4, md5hex/1, shahex/1, tohex/1 ]). %%%---------------------------------------------------------------------- %%% Func: sip_digest/4 %%%---------------------------------------------------------------------- sip_digest(Nonce, Jid, Realm, Passwd) -> HA1 = md5hex(Jid ++ ":" ++ Realm ++ ":" ++ Passwd), HA2 = md5hex("REGISTER:" ++ Jid), INTEGRITY = md5hex(Nonce ++ ":" ++ HA2), HA3 = md5hex(HA1 ++ ":" ++ INTEGRITY), {HA3,INTEGRITY}. %%%---------------------------------------------------------------------- %%% Func: digest/2 %%% Computes XMPP digest password described in JEP-0078 %%%---------------------------------------------------------------------- digest(Sid, Passwd) -> HA1 = shahex(Sid ++ Passwd), {HA1}. %%%---------------------------------------------------------------------- %%% Func: md5hex/1 %%%---------------------------------------------------------------------- md5hex(Clear) -> tohex(binary_to_list(erlang:md5(Clear))). %%%---------------------------------------------------------------------- %%% Func: shahex/1 %%%---------------------------------------------------------------------- shahex(Clear) -> ShaVal= case catch crypto:hash(sha,Clear) of {'EXIT',_} -> crypto:start(), crypto:hash(sha,Clear); Sha -> Sha end, tohex(binary_to_list(ShaVal)). %%%---------------------------------------------------------------------- %%% Func: tohex/1 %%% Purpose: convert list of integers to hexadecimal string %%%---------------------------------------------------------------------- tohex(A)-> Fun = fun(X)-> ts_utils:to_lower(padhex(httpd_util:integer_to_hexlist(X))) end, lists:flatten( lists:map(Fun, A) ). %%%---------------------------------------------------------------------- %%% Func: padhex/1 %%% Purpose: needed because httpd_util:integer_to_hexlist returns hex %%% values <10 as only 1 character, ie. "0F" is simply returned as %%% "F". For our digest, we need these leading zeros to be present. %%% ---------------------------------------------------------------------- padhex(S=[_Char]) -> "0" ++ S; padhex(String) -> String. tsung-1.8.0/src/tsung/ts_cport.erl0000644000201100017670000001430414377756736016670 0ustar nniclausdream%%% %%% Copyright 2009 © Nicolas Niclausse %%% %%% Author : Nicolas Niclausse %%% Created: 17 mar 2009 by Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_cport). -vc('$Id: ts_cport.erl,v 0.0 2009/03/17 10:26:56 nniclaus Exp $ '). -author('nniclausse@niclux.org'). -behaviour(gen_server). -include("ts_macros.hrl"). %% API -export([start_link/1, get_port/2]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -define(EPMD_PORT,4369). -record(state, { min_port = 1025, max_port = 65535 }). %%==================================================================== %% API %%==================================================================== %%-------------------------------------------------------------------- %% Function: start_link() -> {ok,Pid} | ignore | {error,Error} %% Description: Starts the server %%-------------------------------------------------------------------- start_link(Name) -> gen_server:start_link({global, Name}, ?MODULE, [], []). get_port(CPortServer,IP)-> gen_server:call({global,CPortServer},{get,IP}). %%==================================================================== %% gen_server callbacks %%==================================================================== %%-------------------------------------------------------------------- %% Function: init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% Description: Initiates the server %%-------------------------------------------------------------------- init([]) -> %% registering can be long (global:sync needed), do it after the %% init phase (the config_server will send us a message) {Min, Max} = {?config(cport_min),?config(cport_max)}, ts_utils:init_seed(), %% set random port for the initial value. case catch Min+random:uniform(Max-Min) of Val when is_integer(Val) -> ?LOGF("Ok, starting with ~p value~n",[Val],?NOTICE), {ok, #state{min_port=Min, max_port=Max}}; Err -> ?LOGF("ERR starting: ~p~n",[Err],?ERR), {ok, #state{}} end. %%-------------------------------------------------------------------- %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% Description: Handling call messages %%-------------------------------------------------------------------- handle_call({get, ClientIP}, _From, State) -> %% use the process dictionary to store the last port of each ip %% should we use ets instead ? Reply = case get(ClientIP) of ?EPMD_PORT -> ?EPMD_PORT + 1; Val when Val > State#state.max_port -> State#state.min_port; Val -> Val end, put(ClientIP,Reply+1), ?LOGF("Give port number ~p to IP ~p~n",[Reply,ClientIP],?DEB), {reply, Reply, State}. %%-------------------------------------------------------------------- %% Function: handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling cast messages %%-------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% Description: Handling all non call/cast messages %%-------------------------------------------------------------------- handle_info(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% Function: terminate(Reason, State) -> void() %% Description: This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any necessary %% cleaning up. When it returns, the gen_server terminates with Reason. %% The return value is ignored. %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} %% Description: Convert process state when code is changed %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%-------------------------------------------------------------------- %%% Internal functions %%-------------------------------------------------------------------- tsung-1.8.0/src/tsung/ts_client_sup.erl0000644000201100017670000000615714377756736017715 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_client_sup). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -behaviour(supervisor). -include("ts_macros.hrl"). %% External exports -export([start_link/0, start_child/1, active_clients/0]). %% supervisor callbacks -export([init/1]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). start_child(Profile) -> supervisor:start_child(?MODULE,[Profile]). %%%---------------------------------------------------------------------- %%% Callback functions from supervisor %%%---------------------------------------------------------------------- %%-------------------------------------------------------------------- %% @spec active_clients() -> [tuple()] %% @doc returns the list of all active children on this beam's %% client supervisor. @end %%-------------------------------------------------------------------- active_clients()-> length(supervisor:which_children(?MODULE)). %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, {SupFlags, [ChildSpec]}} | %% ignore | %% {error, Reason} %%---------------------------------------------------------------------- init([]) -> ?LOG("Starting ~n", ?INFO), SupFlags = {simple_one_for_one,1, ?restart_sleep}, ChildSpec = [ {ts_client,{ts_client, start, []}, temporary,2000,worker,[ts_client]} ], % fprof:start(), % Res = fprof:trace(start, "/tmp/tsung.fprof"), % ?LOGF("starting profiler: ~p~n",[Res], ?WARN), {ok, {SupFlags, ChildSpec}}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- tsung-1.8.0/src/tsung/ts_client.erl0000644000201100017670000017772214377756736017035 0ustar nniclausdream%%% This code was developed by IDEALX (http://IDEALX.org/) and %%% contributors (their names can be found in the CONTRIBUTORS file). %%% Copyright (C) 2000-2001 IDEALX %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% Created : 15 Feb 2001 by Nicolas Niclausse %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_client). -vc('$Id$ '). -author('nicolas.niclausse@niclux.org'). -modified_by('jflecomte@IDEALX.com'). -behaviour(gen_fsm). % two state: wait_ack | think %%% if bidi is true (for bidirectional), the server can send data %%% to the client at anytime (full bidirectional protocol, as jabber %%% for ex) -include("ts_config.hrl"). -include("ts_profile.hrl"). %% External exports -export([start/1, next/1]). %% gen_server callbacks -export([init/1, wait_ack/2, think/2,handle_sync_event/4, handle_event/3, handle_info/3, terminate/3, code_change/4]). %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- %% @spec start(Opts::{Session::#session{},IP::tuple(),Server::#server{}, %% Id::integer()}) -> {ok, Pid::pid()} | ignore | {error, Error::term()} %% @doc Start a new session start(Opts) -> ?DebugF("Starting with opts: ~p~n",[Opts]), gen_fsm:start_link(?MODULE, Opts, []). %%---------------------------------------------------------------------- %% @spec next({pid()}) -> ok %% @doc Purpose: continue with the next request (used for global ack) %% @end %%---------------------------------------------------------------------- next({Pid}) -> gen_fsm:send_event(Pid, next_msg). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, StateName, State} | %% {ok, StateName, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init(#session{ id = SessionId, persistent = Persistent, bidi = Bidi, hibernate = Hibernate, rate_limit = RateLimit, proto_opts = ProtoOpts, size = Count, client_ip = IP, userid = Id, dump = Dump, seed = Seed, server = Server, type = CType}) -> ?DebugF("Init ... started with count = ~p~n",[Count]), case Seed of now -> {A, B, C} = ?TIMESTAMP, ts_utils:init_seed({A * Id, B, C}); SeedVal when is_integer(SeedVal) -> %% use a different but fixed seed for each client. ts_utils:init_seed({Id,SeedVal}) end, ?DebugF("Get dynparams for ~p~n",[CType]), DynVars = ts_dynvars:new(tsung_userid,Id), StartTime= ?NOW, set_thinktime(?short_timeout), ?DebugF("IP param: ~p~n",[IP]), NewIP = case IP of { TmpIP, -1 } -> {ok, MyHostName} = ts_utils:node_to_hostname(node()), RealIP = case TmpIP of {scan, Interface} -> ts_ip_scan:get_ip(Interface); _ -> TmpIP end, {RealIP, "cport-" ++ MyHostName}; {{scan, Interface}, PortVal } -> ?DebugF("Must scan interface: ~p~n",[Interface]), { ts_ip_scan:get_ip(Interface), PortVal }; Val -> Val end, {RateConf,SizeThresh} = case RateLimit of Token=#token_bucket{} -> Thresh=lists:min([?size_mon_thresh,Token#token_bucket.burst]), {Token#token_bucket{last_packet_date=StartTime}, Thresh}; undefined -> {undefined, ?size_mon_thresh} end, {ok, think, #state_rcv{ port = Server#server.port, host = Server#server.host, session_id = SessionId, bidi = Bidi, protocol = Server#server.type, clienttype = CType, session = CType:new_session(), persistent = Persistent, starttime = StartTime, dump = Dump, proto_opts = ProtoOpts, size_mon = SizeThresh, size_mon_thresh = SizeThresh, count = Count, ip = NewIP, id = Id, hibernate = Hibernate, maxcount = Count, rate_limit = RateConf, dynvars = DynVars }}. %%-------------------------------------------------------------------- %% Func: StateName/2 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%-------------------------------------------------------------------- think(next_msg,State=#state_rcv{}) -> ?LOG("Global ack received, continue~n", ?DEB), NewSocket = (State#state_rcv.protocol):set_opts(State#state_rcv.socket, [{active, once}]), handle_next_action(State#state_rcv{socket=NewSocket }). wait_ack(next_msg,State=#state_rcv{request=R}) when R#ts_request.ack==global-> NewSocket = (State#state_rcv.protocol):set_opts(State#state_rcv.socket, [{active, once}]), {PageTimeStamp, _, _} = update_stats(State), handle_next_action(State#state_rcv{socket=NewSocket, page_timestamp=PageTimeStamp}); wait_ack(timeout,State) -> ?LOG("Error: timeout receive in state wait_ack~n", ?ERR), ts_mon_cache:add({ count, error_timeout }), {stop, normal, State}. %%-------------------------------------------------------------------- %% Func: handle_event/3 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%-------------------------------------------------------------------- handle_event(Event, SName, StateData) -> ?LOGF("Unknown event (~p) received in state ~p, abort",[Event,SName],?ERR), {stop, unknown_event, StateData}. %%-------------------------------------------------------------------- %% Func: handle_sync_event/4 %% Returns: {next_state, NextStateName, NextStateData} | %% {next_state, NextStateName, NextStateData, Timeout} | %% {reply, Reply, NextStateName, NextStateData} | %% {reply, Reply, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} | %% {stop, Reason, Reply, NewStateData} %%-------------------------------------------------------------------- handle_sync_event(_Event, _From, StateName, StateData) -> Reply = ok, {reply, Reply, StateName, StateData}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {next_state, StateName, State} | %% {next_state, StateName, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- %% received data handle_info({erlang, _Socket, Data}, wait_ack, State) -> ?DebugF("erlang function result received: size=~p ~n",[size(term_to_binary(Data))]), case handle_data_msg(Data, State) of {NewState=#state_rcv{ack_done=true}, _Opts} -> handle_next_action(NewState#state_rcv{ack_done=false}); {NewState, _Opts} -> TimeOut = case (NewState#state_rcv.request)#ts_request.ack of global -> (NewState#state_rcv.proto_opts)#proto_opts.global_ack_timeout; _ -> (NewState#state_rcv.proto_opts)#proto_opts.idle_timeout end, {next_state, wait_ack, NewState, TimeOut} end; handle_info(Info, StateName, State = #state_rcv{protocol = Transport, socket = Socket}) -> handle_info2(Transport:normalize_incomming_data(Socket, Info), StateName, State). handle_info2({gen_ts_transport, _Socket, Data}, wait_ack, State=#state_rcv{rate_limit=TokenParam}) when is_binary(Data)-> ?DebugF("data received: size=~p ~n",[size(Data)]), NewTokenParam = case TokenParam of undefined -> undefined; #token_bucket{rate=R,burst=Burst,current_size=S0, last_packet_date=T0} -> {S1,_Wait}=token_bucket(R,Burst,S0,T0,size(Data),?NOW,true), TokenParam#token_bucket{current_size=S1, last_packet_date=?NOW} end, {NewState, Opts} = handle_data_msg(Data, State), NewSocket = (NewState#state_rcv.protocol):set_opts(NewState#state_rcv.socket, [{active, once} | Opts]), case NewState#state_rcv.ack_done of true -> handle_next_action(NewState#state_rcv{socket=NewSocket,rate_limit=NewTokenParam, ack_done=false}); false -> TimeOut = case (NewState#state_rcv.request)#ts_request.ack of global -> (NewState#state_rcv.proto_opts)#proto_opts.global_ack_timeout; _ -> (NewState#state_rcv.proto_opts)#proto_opts.idle_timeout end, {next_state, wait_ack, NewState#state_rcv{socket=NewSocket,rate_limit=NewTokenParam}, TimeOut} end; %% inet close messages; persistent session, waiting for ack handle_info2({gen_ts_transport, _Socket, closed}, wait_ack, State = #state_rcv{persistent=true}) -> ?LOG("connection closed while waiting for ack",?INFO), set_connected_status(false), {NewState, _Opts} = handle_data_msg(closed, State), %% socket should be closed in handle_data_msg handle_next_action(NewState#state_rcv{socket=none}); %% inet close messages; persistent session handle_info2({gen_ts_transport, _Socket, closed}, think, State = #state_rcv{persistent=true}) -> ?LOG("connection closed, stay alive (persistent)",?INFO), set_connected_status(false), catch (State#state_rcv.protocol):close(State#state_rcv.socket), % mandatory for ssl {next_state, think, State#state_rcv{socket = none}}; %% inet close messages handle_info2({gen_ts_transport, _Socket, closed}, _StateName, State) -> ?LOG("connection closed, abort", ?WARN), %% the connection was closed after the last msg was sent, stop quietly ts_mon_cache:add({ count, error_closed }), set_connected_status(false), catch (State#state_rcv.protocol):close(State#state_rcv.socket), % mandatory for ssl {stop, normal, State#state_rcv{socket = none}}; %% inet errors handle_info2({gen_ts_transport, _Socket, error, Reason}, _StateName, State) -> ?LOGF("Net error: ~p~n",[Reason], ?WARN), CountName="error_inet_"++atom_to_list(Reason), ts_mon_cache:add({ count, list_to_atom(CountName) }), set_connected_status(false), {stop, normal, State}; %% timer expires, no more messages to send handle_info2({timeout, _Ref, end_thinktime}, think, State= #state_rcv{ count=0 }) -> ?LOG("Session ending ~n", ?INFO), {stop, normal, State}; %% the timer expires handle_info2({timeout, _Ref, end_thinktime}, think, State ) -> handle_next_action(State); handle_info2(timeout, StateName, State ) -> ?LOGF("Error: timeout receive in state ~p~n",[StateName], ?ERR), ts_mon_cache:add({ count, timeout }), {stop, normal, State}; % bidirectional protocol handle_info2({gen_ts_transport, Socket, Data}, think,State=#state_rcv{ clienttype=Type, bidi=true,host=Host,port=Port}) -> ts_mon:rcvmes({State#state_rcv.dump, self(), Data}), ts_mon_cache:add({ sum, size_rcv, size(Data)}), Proto = State#state_rcv.protocol, ?LOG("Data received from socket (bidi) in state think~n",?INFO), {NextAction, NewState} = case Type:parse_bidi(Data, State) of {nodata, State2, Action} -> ?LOG("Bidi: no data ~n",?DEB), ts_mon_cache:add({count, async_unknown_data_rcv}), {Action, State2}; {Data2, State2, Action} -> ts_mon_cache:add([{ sum, size_sent, size(Data2)},{count, async_data_sent}]), ts_mon:sendmes({State#state_rcv.dump, self(), Data2}), ?LOG("Bidi: send data back to server~n",?DEB), send(Proto,Socket,Data2,Host,Port), %FIXME: handle errors ? {Action, State2} end, NewSocket = (NewState#state_rcv.protocol):set_opts(NewState#state_rcv.socket, [{active, once}]), case NextAction of think -> {next_state, think, NewState#state_rcv{socket=NewSocket}}; continue -> handle_next_action(NewState#state_rcv{socket=NewSocket}) end; % bidi is false, but parse is also false: continue even if we get data handle_info2({gen_ts_transport, Socket, Data}, think, State = #state_rcv{request=Req} ) when (Req#ts_request.ack /= parse) -> ts_mon:rcvmes({State#state_rcv.dump, self(), Data}), ts_mon_cache:add({ sum, size_rcv, size(Data)}), ?LOGF("Data receive from socket in state think, ack=~p, skip~n", [Req#ts_request.ack],?NOTICE), ?DebugF("Data was ~p~n",[Data]), NewSocket = (State#state_rcv.protocol):set_opts(Socket, [{active, once}]), {next_state, think, State#state_rcv{socket=NewSocket}}; handle_info2({gen_ts_transport, _Socket, Data}, think, State) -> ts_mon:rcvmes({State#state_rcv.dump, self(), Data}), ts_mon_cache:add({ count, error_unknown_data }), ?LOG("Data receive from socket in state think, stop~n", ?ERR), ?DebugF("Data was ~p~n",[Data]), {stop, normal, State}; %% pablo TODO: when this could happen?? handle_info2({inet_reply, _Socket,ok}, StateName, State ) -> ?LOGF("inet_reply ok received in state ~p~n",[StateName],?NOTICE), {next_state, StateName, State}; %% TODO this would happen in mixed session when previous session was saved and %% there are data send from the server, and handle_info will NOT normalize %% these data as {gen_ts_transport, Socket, Data}. Ignore it currently. handle_info2({tcp, Socket, _Data}, StateName, State ) -> ?LOGF("tcp data received in state ~p~n",[StateName],?NOTICE), %% we need a set_opts call and update the old socket to the new one, %% or if we switch back to the saved session, we can't receive data %% from the old socket. NewSocket = (State#state_rcv.protocol):set_opts(Socket, [{active, once}]), DictList = get(), lists:foldl(fun({Key, Value}, Acc) -> case {Key, Value} of {{state, _}, {Socket, Session}} -> put(Key, {NewSocket, Session}); _ -> ok end, Acc end, unused, DictList), {next_state, StateName, State}; handle_info2({tcp_closed, Socket}, StateName, State ) -> handle_info2({tcp_closed, Socket, ""}, StateName, State ); handle_info2({tcp_closed, Socket, _Data}, StateName, State ) -> ?LOGF("tcp_closed received in state ~p~n",[StateName],?NOTICE), %% socket closed for the saved session, update the old socket to none. %% it's ok if we don't do that: when the old closed socket is used, %% tsung will aware the closed state, and do a reconnection. DictList = get(), lists:foldl(fun({Key, Value}, Acc) -> case {Key, Value} of {{state, _}, {Socket, Session}} -> put(Key, {none, Session}); _ -> ok end, Acc end, unused, DictList), {next_state, StateName, State}; handle_info2({ssl_closed, Socket}, StateName, State) -> ?LOGF("ssl_closed received in state ~p~n", [StateName], ?NOTICE), % set old socket to none, like when receiving tcp_closed DictList = get(), lists:foldl(fun({Key, Value}, Acc) -> case {Key, Value} of {{state, _}, {Socket, Session}} -> put(Key, {none, Session}); _ -> ok end, Acc end, unused, DictList), {next_state, StateName, State}; handle_info2(Msg, StateName, State ) -> ?LOGF("Error: Unknown msg ~p receive in state ~p, stop~n", [Msg,StateName], ?ERR), ts_mon_cache:add({ count, error_unknown_msg }), {stop, normal, State}. %%-------------------------------------------------------------------- %% Func: terminate/3 %% Purpose: Shutdown the fsm %% Returns: any %%-------------------------------------------------------------------- terminate(normal, _StateName,State) -> finish_session(State); terminate(Reason, StateName, State) -> ?LOGF("Stop in state ~p, reason= ~p~n",[StateName,Reason],?NOTICE), ts_mon_cache:add({ count, error_unknown }), finish_session(State). %%-------------------------------------------------------------------- %% Func: code_change/4 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState, NewStateData} %%-------------------------------------------------------------------- code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: handle_next_action/1 %% Purpose: handle next action: thinktime, transaction or #ts_request %% Args: State %%---------------------------------------------------------------------- handle_next_action(State=#state_rcv{count=0}) -> ?LOG("Session ending ~n", ?INFO), {stop, normal, State}; handle_next_action(State=#state_rcv{dynvars = DynVars}) -> Count = State#state_rcv.count-1, case set_profile(State#state_rcv.maxcount,State#state_rcv.count,State#state_rcv.session_id) of {thinktime, TmpThink} -> Think = case TmpThink of "%%"++_Tail -> Raw=ts_search:subst(TmpThink,DynVars), round(ts_utils:list_to_number(Raw)*1000); Val -> Val end, ?DebugF("Starting new thinktime ~p~n", [Think]), case (set_thinktime(Think) >= State#state_rcv.hibernate) of true -> {next_state, think, State#state_rcv{count=Count},hibernate}; _ -> {next_state, think, State#state_rcv{count=Count}} end; {transaction, start, Tname} -> Now = ?NOW, ?LOGF("Starting new transaction ~p (now~p)~n", [Tname,Now], ?INFO), TrList = State#state_rcv.transactions, NewState = State#state_rcv{transactions=[{Tname,Now}|TrList], count=Count}, handle_next_action(NewState); {transaction, stop, Tname} -> Now = ?NOW, ?LOGF("Stopping transaction ~p (~p)~n", [Tname, Now], ?INFO), TrList = State#state_rcv.transactions, {value, {_, Tr}} = lists:keysearch(Tname, 1, TrList), Elapsed = ts_utils:elapsed(Tr, Now), ts_mon_cache:add({sample, Tname, Elapsed}), NewState = State#state_rcv{transactions=lists:keydelete(Tname,1,TrList), count=Count}, handle_next_action(NewState); {setdynvars,SourceType,Args,VarNames} -> DynVars=State#state_rcv.dynvars, Result = set_dynvars(SourceType,Args,VarNames,DynVars,{State#state_rcv.host,State#state_rcv.port},State#state_rcv.session), NewDynVars = ts_dynvars:set(VarNames,Result,DynVars), ?DebugF("set dynvars: ~p ~n",[NewDynVars]), handle_next_action(State#state_rcv{dynvars = NewDynVars, count=Count}); {ctrl_struct,CtrlData} -> ctrl_struct(CtrlData,State,Count); Request=#ts_request{} -> handle_next_request(Request, State); {change_type, NewCType, Server, Port, PType, Store, Restore, Bidi} -> ?DebugF("Change client type, use: ~p ~p~n",[NewCType, [Server , Port, PType, Store, Restore]]), NewPort = case ts_search:subst(Port,DynVars) of I when is_integer(I) -> I; S when is_list(S) -> list_to_integer(S) end, NewServer = ts_search:subst(Server,DynVars), case Store of true -> % keep state put({state, State#state_rcv.clienttype} , {State#state_rcv.socket,State#state_rcv.session}); false -> % don't keep state of old type, close connection (State#state_rcv.protocol):close(State#state_rcv.socket), set_connected_status(false) end, {Socket,Session} = case {Restore, get({state,NewCType})} of {true,{OldSocket, OldSession}} -> % restore is true and we have something stored {OldSocket, OldSession}; {_,_} -> % nothing to restore, or no restore asked, set new session {none,NewCType:new_session()} end, NewState=State#state_rcv{session=Session,socket=Socket,count=Count,clienttype=NewCType,protocol=PType, port=NewPort,host=NewServer,bidi=Bidi}, handle_next_action(NewState); {set_option, undefined, rate_limit, {Rate, Burst}} -> ?LOGF("Set rate limits for client: rate=~p, burst=~p~n",[Rate,Burst],?DEB), RateConf=#token_bucket{rate=Rate,burst=Burst,last_packet_date=?NOW}, Thresh=lists:min([Burst,State#state_rcv.size_mon_thresh]), handle_next_action(State#state_rcv{size_mon=Thresh,size_mon_thresh=Thresh,rate_limit=RateConf,count=Count}); {set_option, undefined, certificate, {Cacert, KeyFile, KeyPass, CertFile}} -> ?LOGF("Set client certificate: ~p ~p ~p ~p~n",[Cacert, KeyFile, KeyPass, CertFile],?DEB), Opts = ts_utils:filtermap(fun({N,V}) -> case V of undefined -> false; B when is_binary(B)-> {true, {N,ts_search:subst(binary_to_list(B), DynVars)}}; Val -> {true, {N,ts_search:subst(Val, DynVars)}} end end , [{certfile, CertFile}, {keyfile,KeyFile}, {password,KeyPass}, {cacertfile,Cacert}]), ?LOGF("SSL options for certificate: ~p~n",[Opts],?DEB), OldOpts = State#state_rcv.proto_opts, NewOpts = OldOpts#proto_opts{certificate = Opts}, %% close connection if necessary (State#state_rcv.protocol):close(State#state_rcv.socket), set_connected_status(false), handle_next_action(State#state_rcv{proto_opts=NewOpts,count=Count, socket=none}); {set_option, undefined, connect_timeout, {ConnectTimeout}} -> ?LOGF("Set connect timeout: ~p~n", [ConnectTimeout], ?DEB), OldOpts = State#state_rcv.proto_opts, NewOpts = OldOpts#proto_opts{connect_timeout = ConnectTimeout}, handle_next_action(State#state_rcv{proto_opts=NewOpts, count=Count}); {set_option, Type, Name, Args} -> NewState=Type:set_option(Name,Args,State), handle_next_action(NewState); {interaction, send, Id} -> ts_interaction_server:send({ts_search:subst(Id, DynVars),?NOW}), handle_next_action(State#state_rcv{count=Count}); {interaction, 'receive', Id} -> ts_interaction_server:rcv({ts_search:subst(Id, DynVars),?NOW}), handle_next_action(State#state_rcv{count=Count}); {abort, all} -> ?LOGF("Aborting the whole test by request (id is ~p) !!!~n", [State#state_rcv.session_id],?EMERG), ts_config_server:stop(), {stop, normal, State}; {abort, session} -> ?LOGF("Aborting session by request (id is ~p)~n", [State#state_rcv.session_id],?NOTICE), ts_mon_cache:add({ count, abort_session }), {stop, normal, State}; Other -> ?LOGF("Error: set profile return value is ~p (count=~p)~n",[Other,Count],?ERR), {stop, set_profile_error, State} end. %%---------------------------------------------------------------------- %% @spec set_dynvars (Type::erlang|random|urandom|value|file, Args::tuple(), %% Variables::list(), DynVars::#dynvars{}, %% {Server::string(),Port::integer()}, Session::record()) -> integer()|binary()|list() %% @doc setting the value of several dynamic variables at once. %% @end %%---------------------------------------------------------------------- set_dynvars(erlang,{Module,Callback},_Vars,DynVars,_,Session) -> Module:Callback({Session,DynVars}); set_dynvars(code,Fun,_Vars,DynVars,_,Session) -> Fun({Session,DynVars}); set_dynvars(random,{number,Start,End},Vars,_DynVars,_,_) -> lists:map(fun(_) -> ts_stats:uniform(Start,End) end,Vars); set_dynvars(random,{string,Length},Vars,_DynVars,_,_) -> R = fun(_) -> ts_utils:randombinstr(Length) end, lists:map(R,Vars); set_dynvars(urandom,{string,Length},Vars,_DynVars,_,_) -> %% not random, but much faster R = fun(_) -> ts_utils:urandombinstr(Length) end, lists:map(R,Vars); set_dynvars(value,{string,Value},_Vars,_DynVars,_,_) -> [Value]; set_dynvars(file,{random,FileId,Delimiter},_Vars,_DynVars,_,_) -> {ok,Line} = ts_file_server:get_random_line(FileId), ts_utils:split(Line,Delimiter); set_dynvars(file,{iter,FileId,Delimiter},_Vars,_DynVars,_,_) -> {ok,Line} = ts_file_server:get_next_line(FileId), ts_utils:split(Line,Delimiter); set_dynvars(local_file,{FileId,Delimiter},_Vars,_DynVars,_,_) -> {ok,Line} = ts_local_file_server:get_random_line(FileId), ts_utils:split(Line,Delimiter); set_dynvars(jsonpath,{JSONPath, From},_Vars,DynVars,_,_) -> {ok, Val} = ts_dynvars:lookup(From,DynVars), JSON=mochijson2:decode(Val), case ts_utils:jsonpath(JSONPath, JSON) of undefined -> << >>; {struct,S}-> iolist_to_binary(mochijson2:encode({struct,S})); V -> V end; set_dynvars(server,_,_,_,{Host,Port},_) -> [Host,Port]. %% @spec ctrl_struct(CtrlData::term(),State::#state_rcv{},Count::integer) -> %% {next_state, NextStateName::atom(), NextState::#state_rcv{}} | %% {next_state, NextStateName::atom(), NextState::#state_rcv{}, %% Timeout::integer() | infinity} | %% {stop, Reason::term(), NewState::#state_rcv{}} %% @doc Common code for flow control actions (repeat,for) %% Count is the next action-id, if this action doesn't result %% in a jump to another place %% @end ctrl_struct(CtrlData,State,Count) -> case ctrl_struct_impl(CtrlData,State#state_rcv.dynvars) of {next,NewDynVars} -> handle_next_action(State#state_rcv{dynvars=NewDynVars,count=Count}); {jump,Target,NewDynVars} -> %%UGLY HACK: %% because set_profile/3 works by counting down starting at maxcount, %% we need to calculate the correct value to actually make a jump to %% the desired target. %% In set_profile/3, actionId = MaxCount-Count+1 => %% Count = MaxCount-Target +1 Next = State#state_rcv.maxcount - Target + 1, handle_next_action(State#state_rcv{dynvars=NewDynVars,count=Next}) end. %%---------------------------------------------------------------------- %% @spec ctrl_struct_impl(ControlStruct::term(),DynVars::#dynvars{}) -> %% {next,NewDynVars::#dynvars{}} | %% {jump, Target::integer(), NewDynVars::#dynvars{}} %% @doc return {next,NewDynVars} to continue with the sequential flow, %% {jump,Target,NewDynVars} to jump to action number 'Target' %% @end %%---------------------------------------------------------------------- ctrl_struct_impl({for_start,Init="%%_"++_,VarName},DynVars) -> InitialValue = list_to_integer(ts_search:subst(Init, DynVars)), ?LOGF("Initial value of FOR loop is dynamic: ~p",[InitialValue],?DEB), ctrl_struct_impl({for_start,InitialValue,VarName},DynVars); ctrl_struct_impl({for_start,InitialValue,VarName},DynVars) -> NewDynVars = ts_dynvars:set(VarName,InitialValue,DynVars), {next,NewDynVars}; ctrl_struct_impl({for_end,VarName,End="%%_"++_,Increment,Target},DynVars) -> %% end value is a dynamic variable EndValue = list_to_integer(ts_search:subst(End, DynVars)), ?LOGF("End value of FOR loop is dynamic: ~p",[EndValue],?DEB), ctrl_struct_impl({for_end,VarName,EndValue,Increment,Target},DynVars); ctrl_struct_impl({for_end,VarName,EndValue,Increment,Target},DynVars) -> case ts_dynvars:lookup(VarName,DynVars) of {ok,Value} when Value >= EndValue -> % Reach final value, end loop {next,DynVars}; {ok,Value} -> % New iteration NewValue = Value + Increment, NewDynVars = ts_dynvars:set(VarName,NewValue,DynVars), {jump,Target,NewDynVars} end; ctrl_struct_impl({if_start,Rel, VarName, Value, subst, Target},DynVars) -> case ts_dynvars:lookup(VarName,DynVars) of {ok,VarValue} -> NewValue = ts_search:subst(Value, DynVars), ?DebugF("If found ~p; value is ~p; applying substitutions~n",[VarName,VarValue]), ?DebugF("Calling need_jump with args ~p ~p ~p~n",[Rel,NewValue,VarValue]), Jump = need_jump('if',rel(Rel,NewValue,VarValue)), jump_if(Jump,Target,DynVars); false -> ts_mon_cache:add({ count, 'error_if_undef'}), {next,DynVars} end; ctrl_struct_impl({if_start,Rel, VarName, Value, nosubst, Target},DynVars) -> case ts_dynvars:lookup(VarName,DynVars) of {ok,VarValue} -> ?DebugF("If found ~p; value is ~p~n",[VarName,VarValue]), ?DebugF("Calling need_jump with args ~p ~p ~p~n",[Rel,Value,VarValue]), Jump = need_jump('if',rel(Rel,Value,VarValue)), jump_if(Jump,Target,DynVars); false -> ts_mon_cache:add({ count, 'error_if_undef'}), {next,DynVars} end; ctrl_struct_impl({repeat,RepeatName, _,_,_,_,_,_},[]) -> Msg= list_to_atom("error_repeat_"++atom_to_list(RepeatName)++"_undef"), ts_mon_cache:add({ count, Msg}), {next,[]}; ctrl_struct_impl({repeat,RepeatName, While,Rel,VarName,Value,Target,Max},DynVars) -> Iteration = case ts_dynvars:lookup(RepeatName,DynVars) of {ok,Val} -> Val; false -> 1 end, ?DebugF("Repeat (name=~p) iteration: ~p~n",[RepeatName,Iteration]), case Iteration > Max of true -> ?LOGF("Max repeat (name=~p) reached ~p~n",[VarName,Iteration],?NOTICE), ts_mon_cache:add({ count, max_repeat}), {next,DynVars}; false -> case ts_dynvars:lookup(VarName,DynVars) of {ok,VarValue} -> ?DebugF("Repeat (name=~p) found; value is ~p~n",[VarName,VarValue]), ?DebugF("Calling need_jump with args ~p ~p ~p ~p~n",[While,Rel,Value,VarValue]), Jump = need_jump(While,rel(Rel,Value,VarValue)), NewValue = 1 + Iteration, NewDynVars = ts_dynvars:set(RepeatName,NewValue,DynVars), jump_if(Jump,Target,NewDynVars); false -> Msg= list_to_atom("error_repeat_"++atom_to_list(RepeatName)++"undef"), ts_mon_cache:add({ count, Msg}), {next,DynVars} end end; ctrl_struct_impl({foreach_start,ForEachName,VarName,Filter}, DynVars) -> case filter(ts_dynvars:lookup(VarName,DynVars),Filter) of false -> Msg= list_to_atom("error_foreach_"++atom_to_list(VarName)++"undef"), ts_mon_cache:add({ count, Msg}), {next,DynVars}; [First|_Tail] -> TmpDynVars = ts_dynvars:set(ForEachName,First,DynVars), NewDynVars = ts_dynvars:set(ts_utils:concat_atoms([ForEachName,'_iter']),2,TmpDynVars), {next,NewDynVars}; [] -> ?LOGF("empty list for ~p (filter is ~p)",[VarName, Filter],?WARN), NewDynVars = ts_dynvars:set(ts_utils:concat_atoms([ForEachName,'_iter']),1,DynVars), {next,NewDynVars}; VarValue -> ?LOGF("foreach warn:~p is not a list (~p), can't iterate",[VarName, VarValue],?WARN), NewDynVars = ts_dynvars:set(ForEachName,VarValue,DynVars), {next,NewDynVars} end; ctrl_struct_impl({foreach_end,ForEachName,VarName,Filter,Target}, DynVars) -> IterName=ts_utils:concat_atoms([ForEachName,'_iter']), {ok,Iteration} = ts_dynvars:lookup(IterName,DynVars), ?DebugF("Foreach (var=~p) iteration: ~p~n",[VarName,Iteration]), case filter(ts_dynvars:lookup(VarName,DynVars),Filter) of false -> Msg= list_to_atom("error_foreach_"++atom_to_list(VarName)++"undef"), ts_mon_cache:add({ count, Msg}), {next,DynVars}; VarValue when is_list(VarValue)-> ?DebugF("Foreach list found; value is ~p~n",[VarValue]), case catch lists:nth(Iteration,VarValue) of {'EXIT',_} -> % out of bounds, exit foreach loop ?LOGF("foreach ~p: last iteration done",[ForEachName],?DEB), {next,DynVars}; Val -> TmpDynVars = ts_dynvars:set(ForEachName,Val,DynVars), NewDynVars = ts_dynvars:set(IterName,Iteration+1,TmpDynVars), {jump, Target ,NewDynVars} end; _ ->% not a list, don't loop {next,DynVars} end. %% rel(R,A,B) when is_integer(B) and not is_integer(A)-> rel(R,A,list_to_binary(integer_to_list(B))); rel(R,A,B) when is_integer(A) and not is_integer(B)-> rel(R,list_to_binary(integer_to_list(A)),B); rel(R,A,B) when is_list(B) -> rel(R,A,list_to_binary(B)); rel(R,A,B) when is_list(A) -> rel(R,list_to_binary(A),B); rel(R,A,B) when is_atom(A) -> rel(R,atom_to_binary(A,utf8),B); rel(R,A,B) when is_atom(B) -> rel(R,A,atom_to_binary(B,utf8)); rel('eq',A,B) -> A == B; rel('neq',A,B) -> A /= B; rel('gt',A,B) -> binary_to_num(B) > binary_to_num(A); rel('lt',A,B) -> binary_to_num(B) < binary_to_num(A); rel('gte',A,B) -> binary_to_num(B) >= binary_to_num(A); rel('lte',A,B) -> binary_to_num(B) =< binary_to_num(A). need_jump('while',F) -> F; need_jump('until',F) -> not F; need_jump('if',F) -> not F. jump_if(true,Target,DynVars) -> {jump,Target,DynVars}; jump_if(false,_Target,DynVars) -> {next,DynVars}. binary_to_num([H|_T]) -> binary_to_num(H); binary_to_num(Value) -> case (catch list_to_float(binary_to_list(Value))) of {'EXIT', _} -> list_to_integer(binary_to_list(Value)); Float -> Float end. %%---------------------------------------------------------------------- %% Func: handle_next_request/2 %% Args: Request, State %%---------------------------------------------------------------------- handle_next_request(Request, State) -> Count = State#state_rcv.count-1, Type = State#state_rcv.clienttype, {PrevHost, PrevPort, PrevProto} = case Request of #ts_request{host=undefined, port=undefined, scheme=undefined} -> %% host/port/scheme not defined in request, use the current ones. {State#state_rcv.host,State#state_rcv.port, State#state_rcv.protocol}; #ts_request{host=H1, port=P1, scheme=S1} -> {H1,P1,S1} end, {Param, {Host,Port,Protocol}} = case Type:add_dynparams(Request#ts_request.subst, {State#state_rcv.dynvars, State#state_rcv.session}, Request#ts_request.param, {PrevHost, PrevPort, PrevProto}) of {Par, NewServer} -> % substitution has changed server setup ?DebugF("Dynparam, new server: ~p~n",[NewServer]), {Par, NewServer}; P -> {P, {PrevHost, PrevPort, PrevProto}} end, %% need to reconnect if the server/port/scheme has changed Socket = case {State#state_rcv.host,State#state_rcv.port,State#state_rcv.protocol, State#state_rcv.socket} of {Host, Port, Protocol, _} -> % server setup unchanged State#state_rcv.socket; {_,_,_, none} -> ?Debug("Change server configuration inside a session. Socket not opened~n"), set_connected_status(false), none; _ -> ?Debug("Change server configuration inside a session ~n"), (State#state_rcv.protocol):close(State#state_rcv.socket), set_connected_status(false), none end, {Message, NewSession} = Type:get_message(Param,State), Now = ?NOW, %% reconnect if needed ProtoOpts = State#state_rcv.proto_opts, case reconnect(Socket,Host,Port,{Protocol,ProtoOpts},State#state_rcv.ip) of {ok, NewSocket} -> case catch send(Protocol, NewSocket, Message, Host, Port) of ok -> PageTimeStamp = case State#state_rcv.page_timestamp of 0 -> Now; %first request of a page _ -> %page already started State#state_rcv.page_timestamp end, ts_mon_cache:add({ sum, size_sent, size_msg(Message)}), ts_mon:sendmes({State#state_rcv.dump, self(), Message}), NewState = State#state_rcv{socket = NewSocket, protocol = Protocol, host = Host, request = Request, port = Port, count = Count, session = NewSession, proto_opts = ProtoOpts#proto_opts{is_first_connect = false}, page_timestamp= PageTimeStamp, send_timestamp= Now, timestamp= Now }, case Request#ts_request.ack of bidi_ack -> {next_state, think, NewState}; no_ack -> {PTimeStamp, _} = update_stats_noack(NewState), handle_next_action(NewState#state_rcv{ack_done=true, page_timestamp=PTimeStamp}); global -> ts_timer:connected(self()), {next_state, wait_ack, NewState, (NewState#state_rcv.proto_opts)#proto_opts.global_ack_timeout}; _ -> {next_state, wait_ack, NewState, (NewState#state_rcv.proto_opts)#proto_opts.idle_timeout} end; {error, closed} when State#state_rcv.retries < ProtoOpts#proto_opts.max_retries -> ?LOG("connection close while sending message!~n", ?NOTICE), ts_mon_cache:add({ count, error_send_connection_closed }), Retries = State#state_rcv.retries +1, handle_close_while_sending(State#state_rcv{socket=NewSocket, protocol=Protocol, host=Host, session=NewSession, retries=Retries, port=Port}); {error, Reason} when State#state_rcv.retries < ProtoOpts#proto_opts.max_retries -> %% LOG only at INFO level since we report also an error to ts_mon ?LOGF("Error: Unable to send data, reason: ~p~n",[Reason],?INFO), CountName="error_send_"++atom_to_list(Reason), ts_mon_cache:add({ count, list_to_atom(CountName) }), Retries = State#state_rcv.retries +1, handle_timeout_while_sending(State#state_rcv{session=NewSession,retries=Retries}); {'EXIT', {noproc, _Rest}} when State#state_rcv.retries < ProtoOpts#proto_opts.max_retries -> ?LOG("EXIT from ssl app while sending message !~n", ?WARN), Retries = State#state_rcv.retries +1, handle_close_while_sending(State#state_rcv{socket=NewSocket, protocol=Protocol, session=NewSession, retries=Retries, host=Host, port=Port}); Exit when State#state_rcv.retries < ProtoOpts#proto_opts.max_retries -> ?LOGF("EXIT Error: Unable to send data, reason: ~p~n", [Exit], ?ERR), ts_mon_cache:add({ count, error_send }), {stop, normal, State}; _Exit -> ?LOGF("EXIT Error: Unable to send data, max_retries reached; reason: ~p~n", [_Exit], ?ERR), ts_mon_cache:add({ count, error_abort_max_send_retries }), {stop, normal, State} end; {error, timeout} when State#state_rcv.retries < ProtoOpts#proto_opts.max_retries -> ts_mon_cache:add({count, error_connect_timeout}), handle_reconnect_issue(State#state_rcv{session=NewSession}); {error, _Reason} when State#state_rcv.retries < ProtoOpts#proto_opts.max_retries -> handle_reconnect_issue(State#state_rcv{session=NewSession}); {error, Reason} -> case Reason of timeout -> ts_mon_cache:add({count, error_connect_timeout}); closed -> ts_mon_cache:add({count, error_connect_closed}); _ -> ok end, ts_mon_cache:add({ count, error_abort_max_conn_retries }), {stop, normal, State} end. %% @spec size_msg(Data::term) -> integer() size_msg(Data) when is_binary(Data) -> size(Data); size_msg({_Mod,_Fun,_Args,Size}) -> Size. %%---------------------------------------------------------------------- %% Func: finish_session/1 %% Args: State %%---------------------------------------------------------------------- finish_session(State) -> Now = ?NOW, (State#state_rcv.protocol):close(State#state_rcv.socket), set_connected_status(false), Elapsed = ts_utils:elapsed(State#state_rcv.starttime, Now), case State#state_rcv.transactions of [] -> % no pending transactions, do nothing ok; TrList -> % pending transactions (an error has probably occurred) ?LOGF("Pending transactions: ~p, compute transaction time~n",[TrList],?NOTICE), lists:foreach(fun({Tname,StartTime}) -> ts_mon_cache:add({sample,Tname,ts_utils:elapsed(StartTime,Now)}) end, TrList) end, ts_mon:endclient({State#state_rcv.id, ?TIMESTAMP, Elapsed}). %%---------------------------------------------------------------------- %% Func: handle_reconnect_issue/1 %% Args: State %% Purpose: there was an issue (re)opening the connection. Retry with %% backoff in a moment. %%---------------------------------------------------------------------- handle_reconnect_issue(#state_rcv{proto_opts = PO} = State) -> Retries = State#state_rcv.retries + 1, % simplified exponential backoff algorithm: we increase % the timeout when the number of retries increase, with a % simple rule: number of retries * retry_timeout set_thinktime(PO#proto_opts.retry_timeout * Retries), {next_state, think, State#state_rcv{retries=Retries}}. %%---------------------------------------------------------------------- %% Func: handle_close_while_sending/1 %% Args: State %% Purpose: the connection has just be closed a few msec before we %% send a message, restart in a few moment (this time we will %% reconnect before sending) %%---------------------------------------------------------------------- handle_close_while_sending(State=#state_rcv{persistent = true, protocol = Proto, proto_opts = PO})-> Proto:close(State#state_rcv.socket), set_connected_status(false), Think = PO#proto_opts.retry_timeout, %%FIXME: report the error to ts_mon ? ?LOGF("Server must have closed connection upon us, waiting ~p msec~n", [Think], ?NOTICE), set_thinktime(Think), {next_state, think, State#state_rcv{socket=none}}; handle_close_while_sending(State) -> {stop, error, State}. %%---------------------------------------------------------------------- %% Func: handle_timeout_while_sending/1 %% Args: State %% Purpose: retry if a timeout occurs during a send %%---------------------------------------------------------------------- handle_timeout_while_sending(State=#state_rcv{persistent = true, proto_opts = PO})-> Think = PO#proto_opts.retry_timeout, set_thinktime(Think), {next_state, think, State}; handle_timeout_while_sending(State) -> ?LOG("Not persistent, abort client because of send timeout~n", ?INFO), {stop, normal, State}. %%---------------------------------------------------------------------- %% Func: set_profile/2 %% Args: MaxCount, Count (integer), ProfileId (integer) %%---------------------------------------------------------------------- set_profile(MaxCount, Count, ProfileId) when is_integer(ProfileId) -> ts_session_cache:get_req(ProfileId, MaxCount-Count+1). %%---------------------------------------------------------------------- %% Func: reconnect/4 %% Returns: {Socket } | %% {stop, Reason} %% purpose: try to reconnect if this is needed (when the socket is set to none) %%---------------------------------------------------------------------- reconnect(none, ServerName, Port, {Protocol, Proto_opts}, {IP,0}) -> reconnect(none, ServerName, Port, {Protocol, Proto_opts}, {IP,0,0}); reconnect(none, ServerName, Port, {Protocol, Proto_opts}, {IP,CPort, Try}) when is_integer(CPort)-> ?DebugF("Try to (re)connect to: ~p:~p from ~p using protocol ~p~n", [ServerName,Port,IP,Protocol]), Opts = protocol_options(Protocol, Proto_opts) ++ socket_opts(IP, CPort, Protocol), Before= ?NOW, case connect(Protocol, ServerName, Port, Opts, Proto_opts#proto_opts.connect_timeout) of {ok, Socket} -> Elapsed = ts_utils:elapsed(Before, ?NOW), ts_mon_cache:add({ sample, connect, Elapsed }), set_connected_status(true), ?Debug("(Re)connected~n"), {ok, Socket}; {error, timeout} -> % don't handle connect timeouts at this level {error, timeout}; {error, Reason} -> {A,B,C,D} = IP, %% LOG only at INFO level since we report also an error to ts_mon ?LOGF("(Re)connect from ~p.~p.~p.~p:~p to ~s:~p, Error: ~p~n", [A,B,C,D, CPort, ServerName, Port , Reason],?INFO), case {Reason,CPort,Try} of {eaddrinuse, Val,CPortServer} when Val == 0; CPortServer == undefined -> %% already retry once, don't try again. ts_mon_cache:add({ count, error_connect_eaddrinuse }); {eaddrinuse, Val,CPortServer} when Val > 0 -> %% retry once when tsung allocates port number NewCPort = case catch ts_cport:get_port(CPortServer,IP) of Data when is_integer(Data) -> Data; Error -> ?LOGF("CPort error (~p), reuse the same port ~p~n",[Error,CPort],?INFO), CPort end, ?LOGF("Connect failed with client port ~p, retry with ~p~n",[CPort, NewCPort],?INFO), reconnect(none, ServerName, Port, {Protocol, Proto_opts}, {IP,NewCPort, undefined}); _ -> CountName="error_connect_"++atom_to_list(Reason), ts_mon_cache:add({ count, list_to_atom(CountName) }) end, {error, Reason} end; reconnect(none, ServerName, Port, {Protocol, Proto_opts}, {IP,CPortServer}) -> CPort = case catch ts_cport:get_port(CPortServer,IP) of Data when is_integer(Data) -> Data; Error -> ?LOGF("CPort error (~p), use random port~n",[Error],?INFO), 0 end, reconnect(none, ServerName, Port, {Protocol, Proto_opts}, {IP,CPort,CPortServer}); reconnect(Socket, _Server, _Port, _Protocol, _IP) -> {ok, Socket}. %% set options for local socket ip/ports socket_opts({0,0,0,0}, CPort, Proto) when Proto==ts_tcp6 orelse Proto==ts_ssl6 orelse Proto==ts_udp6 -> %% the config server was not aware if we are using ipv6 or ipv4, %% and it set the local IP to be default one; we need to change it %% for ipv6 [{ip, {0,0,0,0,0,0,0,0}},{port,CPort}]; socket_opts(IP, CPort, _)-> [{ip, IP},{port,CPort}]. %%---------------------------------------------------------------------- %% Func: send/5 %% Purpose: wrapper function for send %% Return: ok | {error, Reason} %%---------------------------------------------------------------------- send(Proto, Socket, Message, Host, Port) -> Proto:send(Socket, Message, [{host, Host}, {port, Port}]). connect(Proto, Server, Port, Opts, ConnectTimeout) -> ?LOGF("connect to port ~p",[Port],?DEB), Proto:connect(Server, Port, Opts, ConnectTimeout). %%---------------------------------------------------------------------- %% Func: protocol_options/1 %% Purpose: set connection's options for the given protocol %%---------------------------------------------------------------------- protocol_options(Proto, #proto_opts{} = ProtoOpts) -> Proto:protocol_options(ProtoOpts). %%---------------------------------------------------------------------- %% Func: set_thinktime/1 %% Purpose: set a timer for thinktime if it is not infinite %% returns the chosen thinktime in msec %%---------------------------------------------------------------------- set_thinktime(infinity) -> infinity; set_thinktime(wait_global) -> ts_timer:connected(self()), infinity; set_thinktime({random, Think}) -> set_thinktime(round(ts_stats:exponential(1/Think))); set_thinktime({range, Min, Max}) -> set_thinktime(ts_stats:uniform(Min,Max)); set_thinktime(Think) -> %% dot not use timer:send_after because it does not scale well: %% http://www.erlang.org/ml-archive/erlang-questions/200202/msg00024.html ?DebugF("thinktime of ~p~n",[Think]), erlang:start_timer(Think, self(), end_thinktime ), Think. %%---------------------------------------------------------------------- %% Func: handle_data_msg/2 %% Args: Data (binary), State ('state_rcv' record) %% Returns: {NewState ('state_rcv' record), Socket options (list)} %% Purpose: handle data received from a socket %%---------------------------------------------------------------------- handle_data_msg(Data, State=#state_rcv{request=Req}) when Req#ts_request.ack==no_ack-> ?Debug("data received while previous msg was no_ack~n"), ts_mon:rcvmes({State#state_rcv.dump, self(), Data}), {State, []}; handle_data_msg(Data,State=#state_rcv{dump=Dump,request=Req,id=Id,clienttype=Type,maxcount=MaxCount,transactions=Transactions}) when Req#ts_request.ack==parse-> ts_mon:rcvmes({Dump, self(), Data}), {NewState, Opts, Close} = Type:parse(Data, State), NewBuffer=set_new_buffer(NewState, Data), ?DebugF("Session and dynvars are now ~p ~p~n",[NewState#state_rcv.session, NewState#state_rcv.dynvars]), case NewState#state_rcv.ack_done of true -> ?DebugF("Response done:~p~n", [NewState#state_rcv.datasize]), {PageTimeStamp, DynVars,Elapsed} = update_stats(NewState#state_rcv{buffer=NewBuffer}), MatchArgs={NewState#state_rcv.count, MaxCount, NewState#state_rcv.session_id, Id}, NewDynVars=ts_dynvars:merge(DynVars,NewState#state_rcv.dynvars), NewCount =ts_search:match(Req#ts_request.match,NewBuffer,MatchArgs,NewDynVars,Transactions), Type:dump(Dump,{Req,NewState#state_rcv.session,Id, NewState#state_rcv.host,NewState#state_rcv.datasize,Elapsed,Transactions}), case Close of true -> ?Debug("Close connection required by protocol~n"), (State#state_rcv.protocol):close(State#state_rcv.socket), set_connected_status(false), {NewState#state_rcv{ page_timestamp = PageTimeStamp, socket = none, datasize = 0, size_mon = State#state_rcv.size_mon_thresh, count = NewCount, dynvars = NewDynVars, buffer = <<>>}, Opts}; false -> {NewState#state_rcv{ page_timestamp = PageTimeStamp, count = NewCount, size_mon = State#state_rcv.size_mon_thresh, datasize = 0, dynvars = NewDynVars, buffer = <<>>}, Opts} end; _ -> ?DebugF("Response: continue:~p~n",[NewState#state_rcv.datasize]), %% For size_rcv stats, we don't want to update this stats %% for every packet received (ts_mon will be overloaded), %% so we will update the stats at the end of the %% request. But this is a problem with very big response %% (several megabytes for ex.), because it will create %% artificial spikes in the stats (O B/sec for a long time %% and lot's of MB/s at the end of the req). So we update %% the stats each time a 512Ko threshold is raised. case NewState#state_rcv.datasize > NewState#state_rcv.size_mon of true -> ?Debug("Threshold raised, update size_rcv stats~n"), ts_mon_cache:add({ sum, size_rcv, NewState#state_rcv.size_mon_thresh}), NewThresh=NewState#state_rcv.size_mon+ NewState#state_rcv.size_mon_thresh, {NewState#state_rcv{buffer=NewBuffer,size_mon=NewThresh}, Opts}; false-> {NewState#state_rcv{buffer=NewBuffer}, Opts} end end; handle_data_msg(closed,State) -> {State,[]}; %% ack = global handle_data_msg(Data,State=#state_rcv{request=Req,datasize=OldSize}) when Req#ts_request.ack==global -> %% FIXME: we do not report size now (but after receiving the %% global ack), the size stats may be not very accurate. %% FIXME: should we set buffer and parse for dynvars ? DataSize = size(Data), {State#state_rcv{ datasize = OldSize + DataSize},[]}; %% local ack, special case for jabber: skip keepalive msg (single space char) handle_data_msg(<<32>>, State=#state_rcv{clienttype=ts_jabber}) -> {State#state_rcv{ack_done = false},[]}; %% local ack, set ack_done to true handle_data_msg(Data, State=#state_rcv{request=Req,maxcount=MaxCount,transactions=Transactions}) -> ts_mon:rcvmes({State#state_rcv.dump, self(), Data}), NewBuffer = set_new_buffer(State, Data), DataSize = size(Data), {PageTimeStamp, DynVars,_} = update_stats(State#state_rcv{datasize=DataSize,buffer=NewBuffer}), MatchArgs = {State#state_rcv.count,MaxCount,State#state_rcv.session_id,State#state_rcv.id}, NewDynVars= ts_dynvars:merge(DynVars,State#state_rcv.dynvars), NewCount = ts_search:match(Req#ts_request.match,NewBuffer,MatchArgs,NewDynVars,Transactions), {State#state_rcv{ack_done = true, buffer= NewBuffer, dynvars = NewDynVars, page_timestamp= PageTimeStamp, count=NewCount},[]}. %%---------------------------------------------------------------------- %% Func: set_new_buffer/3 %%---------------------------------------------------------------------- set_new_buffer(#state_rcv{request = #ts_request{match=[], dynvar_specs=[]}} ,_) -> << >>; set_new_buffer(#state_rcv{clienttype=Type, buffer=Buffer, session=Session},closed) -> Type:decode_buffer(Buffer,Session); set_new_buffer(#state_rcv{buffer=OldBuffer,ack_done=false},Data) -> ?Debug("Bufferize response~n"), << OldBuffer/binary, Data/binary >>; set_new_buffer(#state_rcv{clienttype=Type, buffer=OldBuffer, session=Session},Data) when is_binary(Data) -> ?Debug("decode response~n"), Type:decode_buffer(<< OldBuffer/binary, Data/binary >>, Session); set_new_buffer(#state_rcv{clienttype=Type, buffer=OldBuffer, session=Session}, {_M,_F,_A, Res}) when is_list(Res)-> %% erlang fun case Data=list_to_binary(Res), Type:decode_buffer(<< OldBuffer/binary, Data/binary >>, Session); set_new_buffer(_State, Data) -> % useful ? Data. %%---------------------------------------------------------------------- %% Func: set_connected_status/1 %% Args: true|false %% Returns: - %% Purpose: update the statistics for connected users %%---------------------------------------------------------------------- set_connected_status(S) -> set_connected_status(S,get(connected)). set_connected_status(true, true) -> ok; set_connected_status(true, Old) when Old==undefined; Old==false -> put(connected,true), ts_mon_cache:add({sum, connected, 1}); set_connected_status(false, true) -> put(connected,false), ts_mon_cache:add({sum, connected, -1}); set_connected_status(false, Old) when Old==undefined; Old==false -> ok. %%---------------------------------------------------------------------- %% Func: update_stats_noack/1 %% Args: State %% Returns: {TimeStamp, DynVars} %% Purpose: update the statistics for no_ack requests %%---------------------------------------------------------------------- update_stats_noack(#state_rcv{page_timestamp=PageTime,request=Request}) -> Now = ?NOW, Stats= [{ count, request_noack}], % count and not sample because response time is not defined in this case case Request#ts_request.endpage of true -> % end of a page, compute page response time PageElapsed = ts_utils:elapsed(PageTime, Now), ts_mon_cache:add(lists:append([Stats,[{sample, page, PageElapsed}]])), {0, []}; _ -> ts_mon_cache:add(Stats), {PageTime, []} end. %%---------------------------------------------------------------------- %% Func: update_stats/1 %% Args: State %% Returns: {TimeStamp, DynVars} %% Purpose: update the statistics %%---------------------------------------------------------------------- update_stats(S=#state_rcv{size_mon_thresh=T,page_timestamp=PageTime, send_timestamp=SendTime,datasize=Datasize})-> Now = ?NOW, Elapsed = ts_utils:elapsed(SendTime, Now), Stats = case S#state_rcv.size_mon > T of true -> LastSize=Datasize-S#state_rcv.size_mon+T, [{ sample, request, Elapsed}, { sum, size_rcv, LastSize}]; false-> [{ sample, request, Elapsed}, { sum, size_rcv, Datasize}] end, Request = S#state_rcv.request, DynVars = ts_search:parse_dynvar(Request#ts_request.dynvar_specs, S#state_rcv.buffer, S#state_rcv.dynvars), case Request#ts_request.endpage of true -> % end of a page, compute page response time PageElapsed = ts_utils:elapsed(PageTime, Now), ts_mon_cache:add(lists:append([Stats,[{sample, page, PageElapsed}]])), {0, DynVars, Elapsed}; _ -> ts_mon_cache:add(Stats), {PageTime, DynVars, Elapsed} end. filter(false,undefined) -> false; filter({ok,List},undefined)-> List; filter({ok,List},{Include,Re}) when is_list(List)-> Filter=fun(A) -> case re:run(A,Re) of nomatch -> not Include; {match,_} -> Include end end, lists:filter(Filter,List); filter({ok,Data},{Include,Re}) -> filter({ok,[Data]},{Include,Re}). %% @spec token_bucket(R::integer(),Burst::integer(),S0::integer(),T0::tuple(),P1::integer(), %% Now::tuple(),Sleep::boolean()) -> {S1::integer(),Wait::integer()} %% @doc Implement a token bucket to rate limit the traffic: If the %% bucket is full, we wait (if asked) until we can fill the %% bucket with the incoming data %% R = limit rate in Bytes/millisec, Burst = max burst size in Bytes %% T0 arrival date of last packet, %% P1 size in bytes of the packet just received %% S1: new size of the bucket %% Wait: Time to wait %% @end token_bucket(R,Burst,S0,T0,P1,Now,Sleep) -> S1 = lists:min([S0+R*round(ts_utils:elapsed(T0, Now)),Burst]), case P1 < S1 of true -> % no need to wait {S1-P1,0}; false -> % the bucket is full, must wait Wait=(P1-S1) div R, case Sleep of true -> timer:sleep(Wait), {0,Wait}; false-> {0,Wait} end end. tsung-1.8.0/src/tsung/ts_bosh_ssl.erl0000644000201100017670000000403514377756736017355 0ustar nniclausdream%%% %%% Copyright 2010 © ProcessOne %%% %%% Author : Eric Cestari %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_bosh_ssl). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -behaviour(gen_ts_transport). %% This is exactly like ts_bosh, but using ssl instead of plain connections. %% It is easier (fewer tsung modifications required) to have two separate modules, %% and delegate from here to the original. connect(Host, Port, Opts, Timeout) -> ts_bosh:connect(Host, Port, Opts, Timeout, ssl). send(Pid, Data, _Opts) -> ts_bosh:send(Pid, Data, _Opts). close(Pid) -> ts_bosh:close(Pid). set_opts(Pid, _Opts) -> ts_bosh:set_opts(Pid, _Opts). protocol_options(_P) -> ts_bosh:protocol_options(_P). normalize_incomming_data(_Socket, X) -> X. %% nothing to do here, ts_bosh uses a special process to handle http requests, %% the incoming data is already delivered to ts_client as {gen_ts_transport, ..} instead of gen_tcp | ssl tsung-1.8.0/src/tsung/ts_bosh.erl0000644000201100017670000006026614377756736016504 0ustar nniclausdream%%% %%% Copyright 2010 © ProcessOne %%% %%% Author : Eric Cestari %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_bosh). -export([ connect/4, send/3, close/1, set_opts/2, protocol_options/1, normalize_incomming_data/2 ]). -export([connect/5]). %% used for ts_bosh_ssl sessions. -behaviour(gen_ts_transport). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include_lib("xmerl/include/xmerl.hrl"). -define(CONTENT_TYPE, "text/xml; charset=utf-8"). -define(VERSION, "1.8"). -define(WAIT, 60). %1 minute -define(HOLD, 1). %only 1 request pending -define(MAX_QUEUE_SIZE, 5). %% at most 5 messages queued, after that close the connection. %% In practice we never had more than 1 pending packet, as we are blocking %% the client process until we sent the packet. But I keep this functionality in place, %% in case we decide to do the sending() of data asynchronous. -record(state, { host, path, port, % {Host::string(), Port:integer(), Path::string(), Ssl::bool()} domain = undefined, sid, rid, parent_pid, max_requests, %TODO: use this, now fixed on 2 queue = [], %% stanzas that have been queued because we reach the limit of requets open = [], free = [], local_ip, local_port, session_state = fresh, %% fresh | normal | closing pending_ref, connect_timeout = 20 * 1000, type %% 'tcp' | 'ssl' }). normalize_incomming_data(_Socket, X) -> X. %% nothing to do here, ts_bosh uses a special process to handle http requests, %% the incoming data is already delivered to ts_client as {gen_ts_transport, ..} instead of gen_tcp | ssl connect(Host, Port, Opts, Timeout) -> connect(Host, Port, Opts, Timeout, tcp). connect(Host, Port, Opts, Timeout, Type) when Type =:= 'tcp' ; Type =:= 'ssl' -> Parent = self(), [BoshPath | OtherOpts] = Opts, Pid = spawn(fun() -> loop(Host, Port, BoshPath, OtherOpts, Type, Parent, Timeout) end), ?DebugF("connect ~p ~p ~p ~p ~p",[Host, Port, BoshPath, self(), Pid]), {ok, Pid}. extract_domain("to='" ++ Rest) -> lists:takewhile(fun(C) -> C =/= $' end, Rest); extract_domain([_|Rest]) -> extract_domain(Rest). send(Pid, Data, _Opts) -> Ref = make_ref(), Msg = case Data of <<"> -> %%HACK: use this to detect stream start (or restarts) Domain = extract_domain(binary_to_list(Rest)), {stream, Domain, Ref}; <<"", _/binary>> -> %%Use this to detect stream end {stream, terminate, Ref}; _ -> {send, Data, Ref} end, Pid ! Msg, MonitorRef = erlang:monitor(process,Pid), receive {'DOWN', MonitorRef, _Type, _Object, _Info} -> {error, no_bosh_connection}; {ok, Ref} -> erlang:demonitor(MonitorRef, [flush]), ok after 30000 -> erlang:demonitor(MonitorRef, [flush]), {error, timeout} end. close(Pid) when is_pid(Pid)-> Pid ! close; close(none) -> ?LOG("close: no pid",?DEB); close(Pid) -> ?LOGF("close: bad argument: ~p, should be a pid",[Pid],?ERR). set_opts(Pid, _Opts) -> Pid. protocol_options(#proto_opts{bosh_path = BoshPath}) -> [BoshPath]. loop(Host, Port, Path, Opts, Type, Parent, Timeout) -> ts_utils:init_seed(now), _MonitorRef = erlang:monitor(process,Parent), loop(#state{session_state = fresh, port = Port, path = Path, parent_pid = Parent, host = Host, local_ip = proplists:get_value(ip, Opts, undefined), local_port = proplists:get_value(port, Opts, undefined), type = Type, connect_timeout = Timeout }). loop(#state{parent_pid = ParentPid} = State) -> ?DebugF("loop: wait for message free:~p open:~p",[State#state.free, State#state.open]), receive {'DOWN', _MonitorRef, _Type, _Object, _Info} -> %%parent terminates ok; {'EXIT', ParentPid, _Reason} -> %%even 'normal' terminates this ok; close -> ok; {send, Data, Ref} -> case do_send(State, Data) of {sent, NewState} -> ParentPid ! {ok, Ref}, loop(NewState); {queued, #state{queue =Q} = NewState} when length(Q) < ?MAX_QUEUE_SIZE -> %%do not return yet.. loop(NewState#state{pending_ref = Ref}); {queued, NewState} -> %% we reach the max allowed queued messages.. close the connection. ?LOGF("Client reached max bosh requests queue size: ~p. Closing session", [length(NewState#state.queue)], ?ERR), ts_mon_cache:add({count, error_bosh_maxqueued}), ParentPid ! {ok, Ref}, ParentPid ! {gen_ts_transport, self(), closed} end; {stream, terminate, Ref} -> #state{host = Host, path = Path, sid = Sid, rid = Rid, type = Type} = State, {NewState, Socket} = new_socket(State, once), ok = make_raw_request(Type, Socket, Host, Path, close_stream_msg(Sid, Rid)), ParentPid ! {ok, Ref}, loop(NewState#state{session_state = closing, open = [{Socket, Rid+1}|NewState#state.open]}); {stream, Domain, Ref} when State#state.domain == undefined -> NewState = do_connect(State, Domain), ParentPid ! {ok, Ref}, loop(NewState); {stream, _Domain, Ref} -> %%here we must do a reset NewState = do_reset(State), ParentPid ! {ok, Ref}, loop(NewState); {Tag, Socket, {http_response, Vsn, 200, "OK"}} when Tag == 'http' ; Tag == 'ssl'-> ?Debug("loop: http response received"), case do_receive_http_response(State, Socket, Vsn) of {ok, NewState} -> loop(NewState); terminate -> if State#state.session_state /= 'closing' -> ts_mon_cache:add({count, error_bosh_terminated}), ?LOG("Session terminated by server", ?INFO); true -> ok end, State#state.parent_pid ! {gen_ts_transport, self(), closed} end; {Close, Socket} when Close == tcp_closed ; Close == 'ssl_closed' -> ?LOG("loop: close",?DEB), case lists:keymember(Socket, 1, State#state.open) of true -> %%ERROR, a current request is closed ?LOG("Open request closed by server", ?ERR), ts_mon_cache:add({count, error_bosh_socket_closed}), State#state.parent_pid ! {gen_ts_transport, self(), closed}; false -> %% A HTTP persistent connection, currently not in use, is closed by the server. %% We can continue without trouble, just remove it, it will be reopened when needed. loop(State#state{free = lists:delete(Socket, State#state.free)}) end; {Tag, _Socket, {http_response, _Vsn, ResponseCode, _StatusLine}} when Tag == 'http' ; Tag == 'ssl' -> State#state.parent_pid ! {gen_ts_transport, self(), error, list_to_atom(integer_to_list(ResponseCode))}; Unexpected -> ?LOGF("Bosh process received unexpected message: ~p", [Unexpected], ?ERR), State#state.parent_pid ! {gen_ts_transport, self(), error, unexpected_data} end. do_receive_http_response(State, Socket, Vsn) -> #state{open = Open, sid = Sid, rid = Rid, queue = Queue, host = Host, path = Path, type = Type, parent_pid = ParentPid} = State, {ok, {{200, "OK"}, Hdrs, Resp}} = read_response(Type, Socket, Vsn, {200, "OK"}, [], <<>>, httph), ts_mon_cache:add({ sum, size_rcv, iolist_size([ [if is_atom(H) -> atom_to_list(H); true -> H end, V] || {H,V} <- Hdrs])}), %% count header size {_El = #xmlElement{name = body, attributes = Attrs, content = Content}, []}= xmerl_scan:string(binary_to_list(Resp)), case get_attr(Attrs, type) of "terminate" -> terminate; _R -> NewOpen = lists:keydelete(Socket, 1, Open), NewState2 = if NewOpen == [] andalso State#state.session_state =:= 'normal' -> socket_setopts(Type, Socket, [{packet, http}, {active, once}]), ?DebugF("make empty request for normal session state ~p ~p ~p queue:~p", [Type,Socket,Rid, Queue]), ok = make_empty_request(Type, Socket,Sid, Rid, Queue, Host, Path), case length(Queue) of 0 -> ok; _ -> ParentPid ! {ok, State#state.pending_ref} %% we just sent the pending packet, wakeup the client end, State#state{open = [{Socket, Rid}], rid = Rid +1, queue = []}; length(NewOpen) == 1 andalso length(State#state.queue) > 0 -> %%there are pending packet, sent it if the RID is ok, otherwise wait case NewOpen of [{_, R}] when (Rid - R) =< 1 -> socket_setopts(Type, Socket, [{packet, http}, {active, once}]), ok = make_empty_request(Type, Socket,Sid, Rid, Queue, Host, Path), ParentPid ! {ok, State#state.pending_ref}, %% we just sent the pending packet, wakeup the client State#state{open = [{Socket, Rid}], rid = Rid +1, queue = []}; _ -> NewState = return_socket(State, Socket), NewState#state{open = NewOpen} end; true -> NewState = return_socket(State, Socket), NewState#state{open = NewOpen} end, case Content of [] -> %%empty response, do not bother the ts_client process with this %% (so Noack/Bidi won't count this bosh specific thing, only async stanzas) %% since ts_client don't see this, we need to count the size received ts_mon_cache:add({ sum, size_rcv, iolist_size(Resp)}); _ -> ParentPid ! {gen_ts_transport, self(), Resp} end, {ok, NewState2} end. do_connect(#state{type = Type, host = Host, path = Path, parent_pid = ParentPid} = State, Domain) -> ?DebugF("do_connect ~p",[State]), Rid = 1000 + random:uniform(100000), %%Port= proplists:get_value(local_port, Options, undefined), NewState = State#state{ domain = Domain, rid = Rid, open = [], queue = [], free = [] }, {NewState2, Socket} = new_socket(NewState, false), ok = make_raw_request(Type, Socket, Host, Path, create_session_msg(Rid, Domain, ?WAIT, ?HOLD)), {ok, {{200, "OK"}, Hdrs, Resp}} = read_response(Type, Socket, nil, nil, [], <<>>, http), ts_mon_cache:add({ sum, size_rcv, iolist_size([ [if is_atom(H) -> atom_to_list(H); true -> H end, V] || {H,V} <- Hdrs])}), %% count header size NewState3 = return_socket(NewState2, Socket), {_El = #xmlElement{name = body, attributes = Attrs, content = _Content}, []} = xmerl_scan:string(binary_to_list(Resp)), ParentPid ! {gen_ts_transport, self(), Resp}, NewState3#state{rid = Rid +1, open = [], sid = get_attr(Attrs, sid), max_requests = 2 }. do_reset(State) -> ?DebugF("do_reset free: ~p open:~p",[State#state.free,State#state.open]), #state{sid = Sid, rid = Rid, host = Host, path = Path, domain = Domain, type = Type} = State, {NewState, Socket} = new_socket(State, once), ok = make_raw_request(Type, Socket, Host, Path, restart_stream_msg(Sid, Rid, Domain)), NewState#state{session_state = normal, rid = Rid +1, open = [{Socket, Rid}|State#state.open]}. get_attr([], _Name) -> undefined; get_attr([#xmlAttribute{name = Name, value = Value}|_], Name) -> Value; get_attr([_|Rest], Name) -> get_attr(Rest, Name). do_send(State, Data) -> #state{open = Open, rid = Rid, sid = Sid, host = Host, type = Type, path = Path, queue = Queue} = State, ?LOGF("do_send, rid:~p open:~p free:~p", [Rid,Open, State#state.free],?DEB), Result = if Open == [] -> send; true -> Min = lists:min(lists:map(fun({_S,R}) -> R end, Open)), if (Rid -Min) =< 1 -> send; true -> queue end end, case Result of send -> {NewState, Socket} = new_socket(State, once), ok = make_request(Type, Socket, Sid, Rid, Queue, Host, Path, Data), {sent, NewState#state{rid = Rid +1, open = [{Socket, Rid}|Open], queue = Queue}}; queue -> Queue = State#state.queue, NewQueue = [Data|Queue], {queued, State#state{queue = NewQueue}} end. make_empty_request(Type, Socket, Sid, Rid, Queue, Host, Path) -> StanzasText = lists:reverse(Queue), ?LOGF("make empty request ~p ~p ~p", [Type,Socket,Rid],?DEB), Body = stanzas_msg(Sid, Rid, StanzasText), make_request(Type, Socket, Host, Path, Body, iolist_size(StanzasText)). make_raw_request(Type, Socket, Host, Path, Body) -> make_request(Type, Socket, Host, Path, Body, 0). make_request(Type, Socket, Sid, Rid, Queue, Host, Path, Packet) -> StanzasText = lists:reverse([Packet|Queue]), ?LOGF("make request ~p ~p ~p ~p", [Type,Socket,Rid, StanzasText],?DEB), Body = stanzas_msg(Sid, Rid, StanzasText), make_request(Type, Socket, Host, Path, Body, iolist_size(StanzasText)). make_request(Type, Socket,Host, Path, Body, OriginalSize) -> ts_mon_cache:add({count, bosh_http_req}), Hdrs = [{"Content-Type", ?CONTENT_TYPE}, {"keep-alive", "true"}], Request = format_request(Path, "POST", Hdrs, Host, Body), ok = socket_send(Type, Socket, Request), ts_mon_cache:add({ sum, size_sent, iolist_size(Request) - OriginalSize}). %% add the http overhead. The size of the stanzas are already counted by ts_client code. new_socket(State = #state{free = [Socket | Rest], type = Type}, Active) -> socket_setopts(Type, Socket, [{active, Active}, {packet, http}]), {State#state{free = Rest}, Socket}; new_socket(State = #state{type = Type, host = Host, port = Port, local_ip = LocalIp, local_port = LocalPort, connect_timeout=Timeout}, Active) -> Options = case LocalIp of undefined -> [{active, Active}, {packet, http}]; _ -> case LocalPort of undefined -> [{active, Active}, {packet, http},{ip, LocalIp}]; _ -> {ok, LPort} = ts_config_server:get_user_port(LocalIp), [{active, Active}, {packet, http},{ip, LocalIp}, {port, LPort}] end end, {ok, Socket} = socket_connect(Type, Host, Port, Options, Timeout), ts_mon_cache:add({count, bosh_http_conn}), {State, Socket}. return_socket(State, Socket) -> socket_setopts(State#state.type, Socket, [{active, once}]), %%receive data from it, we want to know if something happens State#state{free = [Socket | State#state.free]}. create_session_msg(Rid, To, Wait, Hold) -> [ ""]. stanzas_msg(Sid, Rid, Text) -> [ "", Text, ""]. restart_stream_msg(Sid, Rid, Domain) -> [ ""]. close_stream_msg(Sid, Rid) -> [ ""]. read_response(Type, Socket, Vsn, Status, Hdrs, Body, PacketType) when PacketType == http ; PacketType == httph-> socket_setopts(Type, Socket, [{packet, PacketType}, {active, false}]), case socket_recv(Type, Socket, 0) of {ok, {http_response, NewVsn, StatusCode, Reason}} -> NewStatus = {StatusCode, Reason}, read_response(Type, Socket, NewVsn, NewStatus, Hdrs, Body, httph); {ok, {http_header, _, Name, _, Value}} -> Header = {Name, Value}, read_response(Type, Socket, Vsn, Status, [Header | Hdrs], Body, httph); {ok, http_eoh} -> socket_setopts(Type, Socket, [{packet, raw}, binary]), {NewBody, NewHdrs} = read_body(Type, Vsn, Hdrs, Socket), Response = {Status, NewHdrs, NewBody}, {ok, Response}; {error, closed} -> erlang:error(closed); {error, Reason} -> erlang:error(Reason) end. read_body(Type, _Vsn, Hdrs, Socket) -> % Find out how to read the entity body from the request. % * If we have a Content-Length, just use that and read the complete % entity. % * If Transfer-Encoding is set to chunked, we should read one chunk at % the time % * If neither of this is true, we need to read until the socket is % closed (AFAIK, this was common in versions before 1.1). case proplists:get_value('Content-Length', Hdrs, undefined) of undefined -> case proplists:get_value('Transfer-Encoding', Hdrs, undefined) of undefined -> throw({no_content_length, Hdrs}); "chunked" -> read_chunked_body(Type, Hdrs, Socket, []) end; ContentLength -> read_length(Type, Hdrs, Socket, list_to_integer(ContentLength)) end. read_chunked_body(Type, Hdrs, Socket, Acc) -> socket_setopts(Type, Socket, [{packet, line}, binary]), {ok, HexLen} = socket_recv(Type, Socket, 0), socket_setopts(Type, Socket, [{packet, raw}]), Len = binary_to_integer(binary_part(HexLen, 0, byte_size(HexLen) - 2), 16), case Len of 0 -> {ok, _} = socket_recv(Type, Socket, 2), {iolist_to_binary(lists:reverse(Acc)), Hdrs}; _ -> {ok, Data} = socket_recv(Type, Socket, Len), {ok, _} = socket_recv(Type, Socket, 2), read_chunked_body(Type, [], Socket, [Data|Acc]) end. read_length(Type, Hdrs, Socket, Length) -> case socket_recv(Type, Socket, Length) of {ok, Data} -> {Data, Hdrs}; {error, Reason} -> erlang:error(Reason) end. %% @spec (Path, Method, Headers, Host, Body) -> Request %% Path = iolist() %% Method = atom() | string() %% Headers = [{atom() | string(), string()}] %% Host = string() %% Body = iolist() format_request(Path, Method, Hdrs, Host, Body) -> [ Method, " ", Path, " HTTP/1.1\r\n", format_hdrs(add_mandatory_hdrs(Method, Hdrs, Host, Body), []), Body ]. %% spec normalize_method(AtomOrString) -> Method %% AtomOrString = atom() | string() %% Method = string() %% doc %% Turns the method in to a string suitable for inclusion in a HTTP request %% line. %% end %-spec normalize_method(atom() | string()) -> string(). %normalize_method(Method) when is_atom(Method) -> % string:to_upper(atom_to_list(Method)); %normalize_method(Method) -> % Method. format_hdrs([{Hdr, Value} | T], Acc) -> NewAcc = [ Hdr, ":", Value, "\r\n" | Acc ], format_hdrs(T, NewAcc); format_hdrs([], Acc) -> [Acc, "\r\n"]. add_mandatory_hdrs(Method, Hdrs, Host, Body) -> add_host(add_content_length(Method, Hdrs, Body), Host). add_content_length("POST", Hdrs, Body) -> add_content_length(Hdrs, Body); add_content_length("PUT", Hdrs, Body) -> add_content_length(Hdrs, Body); add_content_length(_, Hdrs, _) -> Hdrs. add_content_length(Hdrs, Body) -> case proplists:get_value("content-length", Hdrs, undefined) of undefined -> ContentLength = integer_to_list(iolist_size(Body)), [{"Content-Length", ContentLength} | Hdrs]; _ -> % We have a content length Hdrs end. add_host(Hdrs, Host) -> case proplists:get_value("host", Hdrs, undefined) of undefined -> [{"Host", Host } | Hdrs]; _ -> % We have a host Hdrs end. socket_connect(tcp, Host, Port, Options, Timeout) -> gen_tcp:connect(Host, Port, Options, Timeout); socket_connect(ssl, Host, Port, Options, Timeout) -> %% First connect using tcp, and then upgrades. The local ip and port directives seems to not work if %% the socket is opened directly as ssl. % {ForConnection, ForSSL} = lists:partition(fun({ip, _}) -> true; ({port, _}) -> true; (_) -> false end, Options), % {ok, S} = gen_tcp:connect(Host, Port, [{active, false}|ForConnection], Timeout), % ssl:connect(S, ForSSL, Timeout). % ?LOGF("Connect ~p", [ForSSL], ?ERR), ssl:connect(Host, Port, [{ssl_imp, new}|Options], Timeout). socket_send(tcp, Socket, Data) -> gen_tcp:send(Socket, Data); socket_send(ssl, Socket, Data) -> ssl:send(Socket, Data). socket_recv(tcp, Socket, Len) -> gen_tcp:recv(Socket, Len); socket_recv(ssl, Socket, Len) -> ssl:recv(Socket, Len). % Not used %socket_close(tcp, Socket) -> % gen_tcp:close(Socket); %socket_close(ssl, Socket) -> % ssl:close(Socket). socket_setopts(tcp, Socket, Opts) -> inet:setopts(Socket, Opts); socket_setopts(ssl, Socket, Opts) -> ssl:setopts(Socket, Opts). tsung-1.8.0/src/tsung/ts_amqp.erl0000644000201100017670000006351714377756736016511 0ustar nniclausdream%%% This code was developed by Zhihui Jiao(jzhihui521@gmail.com). %%% %%% Copyright (C) 2013 Zhihui Jiao %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(ts_amqp). -vc('$Id$ '). -author('jzhihui521@gmail.com'). -behavior(ts_plugin). -include("ts_profile.hrl"). -include("ts_config.hrl"). -include("ts_amqp.hrl"). -include("rabbit.hrl"). -include("rabbit_framing.hrl"). -export([add_dynparams/4, get_message/2, session_defaults/0, parse/2, dump/2, parse_bidi/2, parse_config/2, decode_buffer/2, new_session/0]). %%---------------------------------------------------------------------- %% Function: session_default/0 %% Purpose: default parameters for session %% Returns: {ok, ack_type = parse|no_ack|local, persistent = true|false} %%---------------------------------------------------------------------- session_defaults() -> {ok, true}. %% @spec decode_buffer(Buffer::binary(),Session::record(jabber)) -> %% NewBuffer::binary() %% @doc We need to decode buffer (remove chunks, decompress ...) for %% matching or dyn_variables %% @end decode_buffer(Buffer,#amqp_session{}) -> Buffer. % nothing to do for amqp %%---------------------------------------------------------------------- %% Function: new_session/0 %% Purpose: initialize session information %% Returns: record or [] %%---------------------------------------------------------------------- new_session() -> #amqp_session{map_num_pa = gb_trees:empty(), ack_buf = <<>>}. dump(A,B) -> ts_plugin:dump(A,B). %%---------------------------------------------------------------------- %% Function: get_message/1 %% Purpose: Build a message/request , %% Args: record %% Returns: binary %%---------------------------------------------------------------------- get_message(Request = #amqp_request{channel = ChannelStr}, State) -> ?DebugF("get message on channel: ~p ~p~n", [ChannelStr, Request]), ChannelNum = list_to_integer(ChannelStr), get_message1(Request#amqp_request{channel = ChannelNum}, State). get_message1(#amqp_request{type = connect}, #state_rcv{session = AMQPSession}) -> Waiting = {0, 'connection.start'}, {?PROTOCOL_HEADER, AMQPSession#amqp_session{status = handshake, waiting = Waiting, protocol = ?PROTOCOL}}; get_message1(#amqp_request{type = 'connection.start_ok', username = UserName, password = Password}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, ?DebugF("start with: user=~p, password=~p~n", [UserName, Password]), Resp = plain(none, list_to_binary(UserName), list_to_binary(Password)), StartOk = #'connection.start_ok'{client_properties = client_properties([]), mechanism = <<"PLAIN">>, response = Resp}, Frame = assemble_frame(0, StartOk, Protocol), Waiting = {0, 'connection.tune'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}; get_message1(#amqp_request{type = 'connection.tune_ok', heartbeat = HeartBeat}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, Tune = #'connection.tune_ok'{frame_max = 131072, heartbeat = HeartBeat}, Frame = assemble_frame(0, Tune, Protocol), {Frame, AMQPSession#amqp_session{waiting = none}}; get_message1(#amqp_request{type = 'connection.open', vhost = VHost}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, Open = #'connection.open'{virtual_host = list_to_binary(VHost)}, Frame = assemble_frame(0, Open, Protocol), Waiting = {0, 'connection.open_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}; get_message1(#amqp_request{type = 'channel.open', channel = Channel}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, MapNPA = AMQPSession#amqp_session.map_num_pa, ChannelOpen = #'channel.open'{}, case new_number(Channel, AMQPSession) of {ok, Number} -> MapNPA1 = gb_trees:enter(Number, unused, MapNPA), put({chstate, Number}, #ch{unconfirmed_set = gb_sets:new(), next_pub_seqno = 0}), Frame = assemble_frame(Number, ChannelOpen, Protocol), Waiting = {Number, 'channel.open_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting, map_num_pa = MapNPA1}}; {error, _} -> {<<>>, AMQPSession#amqp_session{waiting = none}} end; get_message1(#amqp_request{type = 'channel.close', channel = Channel}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, ChannelClose = #'channel.close'{reply_text = <<"Goodbye">>, reply_code = 200, class_id = 0, method_id = 0}, Frame = assemble_frame(Channel, ChannelClose, Protocol), Waiting = {Channel, 'channel.close_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}; get_message1(#amqp_request{type = 'confirm.select', channel = Channel}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, Confirm = #'confirm.select'{}, Frame = assemble_frame(Channel, Confirm, Protocol), Waiting = {Channel, 'confirm.select_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}; get_message1(#amqp_request{type = 'basic.qos', prefetch_size = PrefetchSize, channel = Channel, prefetch_count = PrefetchCount}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, Qos = #'basic.qos'{prefetch_size = PrefetchSize, prefetch_count = PrefetchCount}, Frame = assemble_frame(Channel, Qos, Protocol), Waiting = {Channel, 'basic.qos_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}; get_message1(#amqp_request{type = 'basic.publish', channel = Channel, exchange = Exchange, routing_key = RoutingKey, payload_size = Size, payload = Payload, persistent = Persistent}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, MsgPayload = case Payload of "" -> list_to_binary(ts_utils:urandomstr_noflat(Size)); _ -> list_to_binary(Payload) end, Publish = #'basic.publish'{exchange = list_to_binary(Exchange), routing_key = list_to_binary(RoutingKey)}, Msg = case Persistent of true -> Props = #'P_basic'{delivery_mode = 2}, %% persistent message build_content(Props, MsgPayload); false -> Props = #'P_basic'{}, build_content(Props, MsgPayload) end, Frame = assemble_frames(Channel, Publish, Msg, ?FRAME_MIN_SIZE, Protocol), ChState = get({chstate, Channel}), NewChState = case ChState#ch.next_pub_seqno of 0 -> ChState; SeqNo -> USet = ChState#ch.unconfirmed_set, ChState#ch{unconfirmed_set = gb_sets:add(SeqNo, USet), next_pub_seqno = SeqNo + 1} end, put({chstate, Channel}, NewChState), ts_mon_cache:add({count, amqp_published}), {Frame, AMQPSession}; get_message1(#amqp_request{type = 'basic.consume', channel = Channel, queue = Queue, ack = Ack}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, NoAck = case Ack of true -> false; _ -> true end, ConsumerTag = list_to_binary(["tsung-", ts_utils:randombinstr(10)]), Sub = #'basic.consume'{queue = list_to_binary(Queue), consumer_tag = ConsumerTag, no_ack = NoAck}, ChState = get({chstate, Channel}), put({chstate, Channel}, ChState#ch{ack = Ack}), Frame = assemble_frame(Channel, Sub, Protocol), Waiting = {Channel, 'basic.consume_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}; get_message1(#amqp_request{type = 'connection.close'}, #state_rcv{session = AMQPSession}) -> Protocol = AMQPSession#amqp_session.protocol, Close = #'connection.close'{reply_text = <<"Goodbye">>, reply_code = 200, class_id = 0, method_id = 0}, Frame = assemble_frame(0, Close, Protocol), Waiting = {0, 'connection.close_ok'}, {Frame, AMQPSession#amqp_session{waiting = Waiting}}. %%---------------------------------------------------------------------- %% Function: parse/2 %% Purpose: parse the response from the server and keep information %% about the response in State#state_rcv.session %% Args: Data (binary), State (#state_rcv) %% Returns: {NewState, Options for socket (list), Close = true|false} %%---------------------------------------------------------------------- parse(closed, State) -> {State#state_rcv{ack_done = true, datasize = 0}, [], true}; %% new response, compute data size (for stats) parse(Data, State=#state_rcv{acc = [], datasize = 0}) -> parse(Data, State#state_rcv{datasize = size(Data)}); %% handshake stage, parse response, and validate parse(Data, State=#state_rcv{acc = []}) -> do_parse(Data, State); %% more data, add this to accumulator and parse, update datasize parse(Data, State=#state_rcv{acc = Acc, datasize = DataSize}) -> NewSize= DataSize + size(Data), parse(<< Acc/binary, Data/binary >>, State#state_rcv{acc = [], datasize = NewSize}). parse_bidi(<<>>, State=#state_rcv{acc = [], session = AMQPSession}) -> AckBuf = AMQPSession#amqp_session.ack_buf, NewAMQPSession = AMQPSession#amqp_session{ack_buf = <<>>}, ?DebugF("ack buf: ~p~n", [AckBuf]), {confirm_ack_buf(AckBuf), State#state_rcv{session = NewAMQPSession},think}; parse_bidi(Data, State=#state_rcv{acc = [], session = AMQPSession}) -> ?DebugF("parse bidi data: ~p ~p~n", [size(Data), Data]), Protocol = AMQPSession#amqp_session.protocol, AckBuf = AMQPSession#amqp_session.ack_buf, case decode_frame(Protocol, Data) of {error, _Reason} -> ?DebugF("decode error: ~p~n", [_Reason]), {nodata, State, think}; {ok, heartbeat, Left} -> ?DebugF("receive bidi: ~p~n", [heartbeat]), HB = list_to_binary(rabbit_binary_generator:build_heartbeat_frame()), NewAckBuf = <>, NewAMQPSession = AMQPSession#amqp_session{ack_buf = NewAckBuf}, parse_bidi(Left, State#state_rcv{session = NewAMQPSession}); {ok, _, none, Left} -> parse_bidi(Left, State); {ok, Channel, Method, Left} -> ?DebugF("receive bidi: ~p ~p~n", [Channel, Method]), NewAMQPSession = should_ack(Channel, AckBuf, Method, AMQPSession), parse_bidi(Left, State#state_rcv{session = NewAMQPSession}); {incomplete, Left} -> ?DebugF("incomplete frame: ~p~n", [Left]), {confirm_ack_buf(AckBuf), State#state_rcv{acc = Left},think} end; parse_bidi(Data, State=#state_rcv{acc = Acc, datasize = DataSize, session = AMQPSession}) -> NewSize = DataSize + size(Data), ?DebugF("parse bidi data: ~p ~p~n", [NewSize, Data, Acc]), parse_bidi(<>, State#state_rcv{acc = [], datasize = NewSize, session = AMQPSession#amqp_session{ack_buf = <<>>}}). %%---------------------------------------------------------------------- %% Function: parse_config/2 %% Purpose: parse tags in the XML config file related to the protocol %% Returns: List %%---------------------------------------------------------------------- parse_config(Element, Conf) -> ts_config_amqp:parse_config(Element, Conf). %%---------------------------------------------------------------------- %% Function: add_dynparams/4 %% Purpose: we dont actually do anything %% Returns: #amqp_request %%---------------------------------------------------------------------- add_dynparams(false, {_DynVars, _Session}, Param, _HostData) -> Param; add_dynparams(true, {DynVars, _Session}, Req = #amqp_request{channel = Channel, payload = Payload, exchange = Exchange, routing_key = RoutingKey, queue = Queue}, _HostData) -> SubstChannel = ts_search:subst(Channel, DynVars), SubstPayload = ts_search:subst(Payload, DynVars), SubstExchange = ts_search:subst(Exchange, DynVars), SubstRoutingKey = ts_search:subst(RoutingKey, DynVars), SubstQueue = ts_search:subst(Queue, DynVars), Req#amqp_request{channel = SubstChannel, payload = SubstPayload, exchange = SubstExchange, routing_key = SubstRoutingKey, queue = SubstQueue}. %%---------------------------------------------------------------------- plain(none, Username, Password) -> <<0, Username/binary, 0, Password/binary>>. do_parse(Data, State = #state_rcv{session = AMQPSession}) -> ?DebugF("start do_parse: ~p ~n", [Data]), Protocol = AMQPSession#amqp_session.protocol, Waiting = AMQPSession#amqp_session.waiting, case decode_and_check(Data, Waiting, State, Protocol) of {ok, _Method, Result} -> Result; {fail, Result} -> Result end. get_post_fun(_Channel, 'connection.open_ok') -> fun({NewState, Options, Close}) -> AMQPSession = NewState#state_rcv.session, NewAMQPSession = AMQPSession#amqp_session{status = connected}, NewState1 = NewState#state_rcv{session = NewAMQPSession}, ts_mon_cache:add({count, amqp_connected}), {NewState1, Options, Close} end; get_post_fun(_Channel, 'channel.open_ok') -> fun({NewState, Options, Close}) -> ts_mon_cache:add({count, amqp_channel_opened}), {NewState, Options, Close} end; get_post_fun(_Channel, 'channel.close_ok') -> fun({NewState, Options, Close}) -> ts_mon_cache:add({count, amqp_channel_closed}), {NewState, Options, Close} end; get_post_fun(Channel, 'confirm.select_ok') -> fun({NewState, Options, Close}) -> ChState = get({chstate, Channel}), NewChState = ChState#ch{next_pub_seqno = 1}, put({chstate, Channel}, NewChState), NewState1 = NewState#state_rcv{acc = []}, {NewState1, Options, Close} end; get_post_fun(_Channel, 'basic.consume_ok') -> fun({NewState, Options, Close}) -> AMQPSession = NewState#state_rcv.session, Socket = NewState#state_rcv.socket, ts_mon_cache:add({count, amqp_consumer}), LeftData = NewState#state_rcv.acc, NewAMQPSession = AMQPSession#amqp_session{waiting = none}, NewState1 = NewState#state_rcv{acc = [], session = NewAMQPSession}, case LeftData of <<>> -> ok; %% trick, trigger the parse_bidi call _ -> self() ! {gen_ts_transport, Socket, LeftData} end, {NewState1, Options, Close} end; get_post_fun(_Channel, 'connection.close_ok') -> fun({NewState, Options, _Close}) -> ts_mon_cache:add({count, amqp_closed}), {NewState, Options, true} end; get_post_fun(_Channel, _) -> fun({NewState, Options, Close}) -> AMQPSession = NewState#state_rcv.session, NewAMQPSession = AMQPSession#amqp_session{waiting = none}, NewState1 = NewState#state_rcv{session = NewAMQPSession}, {NewState1, Options, Close} end. new_number(0, #amqp_session{channel_max = ChannelMax, map_num_pa = MapNPA}) -> case gb_trees:is_empty(MapNPA) of true -> {ok, 1}; false -> {Smallest, _} = gb_trees:smallest(MapNPA), if Smallest > 1 -> {ok, Smallest - 1}; true -> {Largest, _} = gb_trees:largest(MapNPA), if Largest < ChannelMax -> {ok, Largest + 1}; true -> find_free(MapNPA) end end end; new_number(Proposed, Session = #amqp_session{channel_max = ChannelMax, map_num_pa = MapNPA}) -> IsValid = Proposed > 0 andalso Proposed =< ChannelMax andalso not gb_trees:is_defined(Proposed, MapNPA), case IsValid of true -> {ok, Proposed}; false -> new_number(none, Session) end. find_free(MapNPA) -> find_free(gb_trees:iterator(MapNPA), 1). find_free(It, Candidate) -> case gb_trees:next(It) of {Number, _, It1} -> if Number > Candidate -> {ok, Number - 1}; Number =:= Candidate -> find_free(It1, Candidate + 1) end; none -> {error, out_of_channel_numbers} end. confirm_ack_buf(AckBuf) -> case AckBuf of <<>> -> nodata; _ -> AckBuf end. should_ack(Channel, AckBuf, #'basic.deliver'{delivery_tag = DeliveryTag}, AMQPSession = #amqp_session{protocol = Protocol}) -> ChState = get({chstate, Channel}), case ChState#ch.ack of true -> ?DebugF("delivered: ~p ~n", [ack]), Ack = #'basic.ack'{delivery_tag = DeliveryTag}, Frame = assemble_frame(Channel, Ack, Protocol), ts_mon_cache:add({count, amqp_delivered}), NewAckBuf = case AckBuf of nodata -> Frame; _ -> <> end, AMQPSession#amqp_session{ack_buf = NewAckBuf}; false -> ?DebugF("delivered: ~p ~n", [noack]), ts_mon_cache:add({count, amqp_delivered}), AMQPSession#amqp_session{ack_buf = AckBuf} end; should_ack(Channel, AckBuf, Method = #'basic.ack'{}, AMQPSession) -> ?DebugF("publish confirm: ~p ~n", [ack]), update_confirm_set(Channel, Method), AMQPSession#amqp_session{ack_buf = AckBuf}; should_ack(Channel, AckBuf, Method = #'basic.nack'{}, AMQPSession) -> ?DebugF("publish confirm: ~p ~n", [nack]), update_confirm_set(Channel, Method), AMQPSession#amqp_session{ack_buf = AckBuf}; should_ack(_Channel, AckBuf, _Method, AMQPSession) -> ?DebugF("delivered: ~p ~n", [other]), AMQPSession#amqp_session{ack_buf = AckBuf}. update_confirm_set(Channel, #'basic.ack'{delivery_tag = SeqNo, multiple = Multiple}) -> ChState = get({chstate, Channel}), USet = ChState#ch.unconfirmed_set, USet1 = update_unconfirmed(ack, SeqNo, Multiple, USet), put({chstate, Channel}, ChState#ch{unconfirmed_set = USet1}); update_confirm_set(Channel, #'basic.nack'{delivery_tag = SeqNo, multiple = Multiple}) -> ChState = get({chstate, Channel}), USet = ChState#ch.unconfirmed_set, USet1 = update_unconfirmed(nack, SeqNo, Multiple, USet), put({chstate, Channel}, ChState#ch{unconfirmed_set = USet1}). update_unconfirmed(AckType, SeqNo, false, USet) -> add_ack_stat(AckType), gb_sets:del_element(SeqNo, USet); update_unconfirmed(AckType, SeqNo, true, USet) -> case gb_sets:is_empty(USet) of true -> USet; false -> {S, USet1} = gb_sets:take_smallest(USet), case S > SeqNo of true -> USet; false -> add_ack_stat(AckType), update_unconfirmed(AckType, SeqNo, true, USet1) end end. add_ack_stat(ack) -> ts_mon_cache:add({count, amqp_confirmed}); add_ack_stat(nack) -> ts_mon_cache:add({count, amqp_unconfirmed}). client_properties(UserProperties) -> Default = [{<<"product">>, longstr, <<"Tsung">>}, {<<"version">>, longstr, list_to_binary("0.0.1")}, {<<"platform">>, longstr, <<"Erlang">>}, {<<"capabilities">>, table, ?CLIENT_CAPABILITIES}], lists:foldl(fun({K, _, _} = Tuple, Acc) -> lists:keystore(K, 1, Acc, Tuple) end, Default, UserProperties). assemble_frame(Channel, MethodRecord, Protocol) -> list_to_binary(rabbit_binary_generator:build_simple_method_frame( Channel, MethodRecord, Protocol)). assemble_frames(Channel, MethodRecord, Content, FrameMax, Protocol) -> MethodName = rabbit_misc:method_record_type(MethodRecord), true = Protocol:method_has_content(MethodName), % assertion MethodFrame = rabbit_binary_generator:build_simple_method_frame( Channel, MethodRecord, Protocol), ContentFrames = rabbit_binary_generator:build_simple_content_frames( Channel, Content, FrameMax, Protocol), list_to_binary([MethodFrame | ContentFrames]). build_content(Properties, BodyBin) when is_binary(BodyBin) -> build_content(Properties, [BodyBin]); build_content(Properties, PFR) -> %% basic.publish hasn't changed so we can just hard-code amqp_0_9_1 {ClassId, _MethodId} = rabbit_framing_amqp_0_9_1:method_id('basic.publish'), #content{class_id = ClassId, properties = Properties, properties_bin = none, protocol = none, payload_fragments_rev = PFR}. decode_and_check(Data, Waiting, State, Protocol) -> case decode_frame(Protocol, Data) of {error, _Reason} -> ?DebugF("decode error: ~p~n", [_Reason]), ts_mon_cache:add({count, amqp_error}), {fail, {State#state_rcv{ack_done = true}, [], true}}; {ok, heartbeat, Left} -> {ok, heartbeat, {State#state_rcv{ack_done = false, acc = Left}, [], true}}; {ok, Channel, Method, Left} -> check(Channel, Waiting, Method, State, Left); {incomplete, Left} -> ?DebugF("incomplete frame: ~p~n", [Left]), {fail, {State#state_rcv{ack_done = false, acc = Left}, [], false}} end. check(Channel, {Channel, Expecting}, Method, State, Left) -> ?DebugF("receive from server: ~p~n", [Method]), case {Expecting, element(1, Method)} of {E, M} when E =:= M -> PostFun = get_post_fun(Channel, Expecting), {ok, Method, PostFun({State#state_rcv{ack_done = true, acc = Left}, [], false})}; _ -> ts_mon_cache:add({count, amqp_unexpected}), ?DebugF("unexpected_method: ~p, expecting ~p~n", [Method, Expecting]), {fail, {State#state_rcv{ack_done = true}, [], true}} end; check(Channel, Waiting = {WaitingCh, Expecting}, Method = #'basic.deliver'{}, State = #state_rcv{session = AMQPSession}, Left) -> ?LOGF("waiting on ~p, expecting ~p, but receive deliver on ~p ~p~n", [WaitingCh, Expecting, Channel, Method], ?NOTICE), AckBuf = AMQPSession#amqp_session.ack_buf, NewAMQPSession = should_ack(Channel, AckBuf, Method, AMQPSession), Protocol = AMQPSession#amqp_session.protocol, decode_and_check(Left, Waiting, State#state_rcv{session = NewAMQPSession}, Protocol); check(Channel, Waiting = {WaitingCh, Expecting}, Method, State = #state_rcv{session = AMQPSession}, Left) -> ?LOGF("waiting on ~p, but received on ~p, expecting: ~p, actual: ~p~n", [WaitingCh, Channel, Expecting, Method], ?NOTICE), Protocol = AMQPSession#amqp_session.protocol, decode_and_check(Left, Waiting, State, Protocol). decode_frame(Protocol, <>) when size(Body) > Length -> <> = Body, case rabbit_command_assembler:analyze_frame(Type, PayLoad, Protocol) of heartbeat -> {ok, heartbeat, Left}; AnalyzedFrame -> process_frame(AnalyzedFrame, Channel, Protocol, Left) end; decode_frame(_Protocol, Data) -> {incomplete, Data}. process_frame(Frame, Channel, Protocol, Left) -> AState = case get({channel, Channel}) of undefined -> {ok, InitAState} = rabbit_command_assembler:init(Protocol), InitAState; AState1-> AState1 end, case process_channel_frame(Frame, AState, Left) of {ok, Method, NewAState, Left} -> put({channel, Channel}, NewAState), {ok, Channel, Method, Left}; Other -> Other end. process_channel_frame(Frame, AState, Left) -> case rabbit_command_assembler:process(Frame, AState) of {ok, NewAState} -> {ok, none, NewAState, Left}; {ok, Method, NewAState} -> {ok, Method, NewAState, Left}; {ok, Method, _Content, NewAState} -> {ok, Method, NewAState, Left}; {error, Reason} -> {error, Reason} end. tsung-1.8.0/src/tsung/gen_ts_transport.erl0000644000201100017670000000266014377756736020430 0ustar nniclausdream%%% Copyright (C) 2012 Nicolas Niclausse %%% %%% This program is free software; you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation; either version 2 of the License, or %%% (at your option) any later version. %%% %%% This program is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with this program; if not, write to the Free Software %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. %%% %%% Created : 7 Sep 2012 by Nicolas Niclausse %%% In addition, as a special exception, you have the permission to %%% link the code of this program with any library released under %%% the EPL license and distribute linked combinations including %%% the two; the MPL (Mozilla Public License), which EPL (Erlang %%% Public License) is based on, is included in this exception. -module(gen_ts_transport). -export([behaviour_info/1]). behaviour_info(callbacks) -> [{connect, 4}, {send, 3}, {close, 1}, {set_opts, 2}, {protocol_options, 1}, {normalize_incomming_data, 2}]; behaviour_info(_Other) -> undefined.