tsung-1.7.0/0000755000201100017670000000000013151461561012347 5ustar nniclausdreamtsung-1.7.0/ebin/0000755000201100017670000000000013151461561013264 5ustar nniclausdreamtsung-1.7.0/tsung-recorder.sh.in0000644000201100017670000001136513151315546016262 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.7.0/tsung.spec0000644000201100017670000000462013151461536014367 0ustar nniclausdream%define name tsung %define version 1.7.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.7.0/tsung.spec.in0000644000201100017670000000463413151315546015000 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.7.0/install-sh0000755000201100017670000001272013151315546014356 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.7.0/Makefile.in0000644000201100017670000002777613151315546014440 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.7.0/config.sub0000644000201100017670000007511313151315546014337 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.7.0/aclocal.m40000644000201100017670000000114413151315546014210 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.7.0/acinclude.m40000755000201100017670000000255313151315546014551 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.7.0/config.guess0000644000201100017670000012450213151315546014671 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 tupples: *-*-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.7.0/configure.ac0000644000201100017670000000761513151315546014647 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.7.0/configure0000755000201100017670000037723513151461550014275 0ustar nniclausdream#! /bin/sh # Guess values for system-dependent variables and create Makefiles. # Generated by GNU Autoconf 2.69 for tsung 1.7.0. # # Report bugs to . # # # Copyright (C) 1992-1996, 1998-2012 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 if test -n "${ZSH_VERSION+set}" && (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 case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi as_nl=' ' export as_nl # Printing a long string crashes Solaris 7 /usr/bin/printf. as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo # Prefer a ksh shell builtin over an external printf program on Solaris, # but without wasting forks for bash or zsh. if test -z "$BASH_VERSION$ZSH_VERSION" \ && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='print -r --' as_echo_n='print -rn --' elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='printf %s\n' as_echo_n='printf %s' else if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' as_echo_n='/usr/ucb/echo -n' else as_echo_body='eval expr "X$1" : "X\\(.*\\)"' as_echo_n_body='eval arg=$1; case $arg in #( *"$as_nl"*) expr "X$arg" : "X\\(.*\\)$as_nl"; arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; esac; expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" ' export as_echo_n_body as_echo_n='sh -c $as_echo_n_body as_echo' fi export as_echo_body as_echo='sh -c $as_echo_body as_echo' fi # The user is always right. if test "${PATH_SEPARATOR+set}" != set; 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 # IFS # We need space, tab and new line, in precisely that order. Quoting is # there to prevent editors from complaining about space-tab. # (If _AS_PATH_WALK were called with IFS unset, it would disable word # splitting by setting IFS to empty value.) IFS=" "" $as_nl" # 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 test -z "$as_dir" && as_dir=. 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 $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi # Unset variables that we do not need and which cause bugs (e.g. in # pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" # suppresses any "Segmentation fault" message there. '((' could # trigger a bug in pdksh 5.2.14. for as_var in BASH_ENV ENV MAIL MAILPATH do eval test x\${$as_var+set} = xset \ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : done PS1='$ ' PS2='> ' PS4='+ ' # NLS nuisances. LC_ALL=C export LC_ALL LANGUAGE=C export LANGUAGE # CDPATH. (unset CDPATH) >/dev/null 2>&1 && unset CDPATH # 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'. $as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 as_fn_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="if test -n \"\${ZSH_VERSION+set}\" && (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 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 exitcode=1; echo positional parameters were not saved. fi test x\$exitcode = x0 || 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_have_required=no fi if test x$as_have_required = xyes && (eval "$as_suggested") 2>/dev/null; then : else 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 test -z "$as_dir" && as_dir=. 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_echo "$as_bourne_compatible""$as_required" | as_run=a "$as_shell"; } 2>/dev/null; then : CONFIG_SHELL=$as_shell as_have_required=yes if { $as_echo "$as_bourne_compatible""$as_suggested" | as_run=a "$as_shell"; } 2>/dev/null; then : break 2 fi fi done;; esac as_found=false done $as_found || { if { test -f "$SHELL" || test -f "$SHELL.exe"; } && { $as_echo "$as_bourne_compatible""$as_required" | as_run=a "$SHELL"; } 2>/dev/null; then : CONFIG_SHELL=$SHELL as_have_required=yes fi; } IFS=$as_save_IFS 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'. $as_echo "$0: could not re-execute with $CONFIG_SHELL" >&2 exit 255 fi if test x$as_have_required = xno; then : $as_echo "$0: This script requires a shell more modern than all" $as_echo "$0: the shells that I found on your system." if test x${ZSH_VERSION+set} = xset ; then $as_echo "$0: In particular, zsh $ZSH_VERSION has bugs and should" $as_echo "$0: be upgraded to zsh 4.3.4 or later." else $as_echo "$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_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=`$as_echo "$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 || $as_echo 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_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_fn_arith () { as_val=`expr "$@" || test $? -eq 1` } fi # as_fn_arith # 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 $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi $as_echo "$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 || $as_echo 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" || { $as_echo "$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 } 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 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.7.0' PACKAGE_STRING='tsung 1.7.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 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' 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 # Accept the important Cygnus configure options, so we can diagnose typos. 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=`$as_echo "$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=`$as_echo "$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 ;; -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=`$as_echo "$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=`$as_echo "$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. $as_echo "$as_me: WARNING: you should use --build, --host, --target" >&2 expr "x$ac_option" : ".*[^-._$as_cr_alnum]" >/dev/null && $as_echo "$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" ;; *) $as_echo "$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 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 || $as_echo 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.7.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] --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.7.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=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. ac_top_builddir_sub=`$as_echo "$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 guested configure. 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 $as_echo "$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.7.0 generated by GNU Autoconf 2.69 Copyright (C) 2012 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 link 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\"" $as_echo "$ac_try_echo"; } >&5 (eval "$ac_link") 2>&5 ac_status=$? $as_echo "$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\"" $as_echo "$ac_try_echo"; } >&5 (eval "$ac_try") 2>&5 ac_status=$? $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 test $ac_status = 0; }; }; then : ac_retval=0 else $as_echo "$as_me: program exited with status $ac_status" >&5 $as_echo "$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 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.7.0, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ _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 test -z "$as_dir" && as_dir=. $as_echo "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=`$as_echo "$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=$? # Save into config.log some information that might help in debugging. { echo $as_echo "## ---------------- ## ## 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_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 $as_echo "$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 $as_echo "## ----------------- ## ## Output variables. ## ## ----------------- ##" echo for ac_var in $ac_subst_vars do eval ac_val=\$$ac_var case $ac_val in *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac $as_echo "$ac_var='\''$ac_val'\''" done | sort echo if test -n "$ac_subst_files"; then $as_echo "## ------------------- ## ## File substitutions. ## ## ------------------- ##" echo for ac_var in $ac_subst_files do eval ac_val=\$$ac_var case $ac_val in *\'\''*) ac_val=`$as_echo "$ac_val" | sed "s/'\''/'\''\\\\\\\\'\'''\''/g"`;; esac $as_echo "$ac_var='\''$ac_val'\''" done | sort echo fi if test -s confdefs.h; then $as_echo "## ----------- ## ## confdefs.h. ## ## ----------- ##" echo cat confdefs.h echo fi test "$ac_signal" != 0 && $as_echo "$as_me: caught signal $ac_signal" $as_echo "$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 $as_echo "/* confdefs.h */" > confdefs.h # Predefined preprocessor variables. cat >>confdefs.h <<_ACEOF #define PACKAGE_NAME "$PACKAGE_NAME" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_TARNAME "$PACKAGE_TARNAME" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_VERSION "$PACKAGE_VERSION" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_STRING "$PACKAGE_STRING" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_BUGREPORT "$PACKAGE_BUGREPORT" _ACEOF cat >>confdefs.h <<_ACEOF #define PACKAGE_URL "$PACKAGE_URL" _ACEOF # Let the site file select an alternate cache file if it wants to. # Prefer an explicitly selected file to automatically selected ones. ac_site_file1=NONE ac_site_file2=NONE if test -n "$CONFIG_SITE"; then # We do not want a PATH search for config.site. case $CONFIG_SITE in #(( -*) ac_site_file1=./$CONFIG_SITE;; */*) ac_site_file1=$CONFIG_SITE;; *) ac_site_file1=./$CONFIG_SITE;; esac elif test "x$prefix" != xNONE; then ac_site_file1=$prefix/share/config.site ac_site_file2=$prefix/etc/config.site else ac_site_file1=$ac_default_prefix/share/config.site ac_site_file2=$ac_default_prefix/etc/config.site fi for ac_site_file in "$ac_site_file1" "$ac_site_file2" do test "x$ac_site_file" = xNONE && continue if test /dev/null != "$ac_site_file" && test -r "$ac_site_file"; then { $as_echo "$as_me:${as_lineno-$LINENO}: loading site script $ac_site_file" >&5 $as_echo "$as_me: loading site script $ac_site_file" >&6;} sed 's/^/| /' "$ac_site_file" >&5 . "$ac_site_file" \ || { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 $as_echo "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else { $as_echo "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 $as_echo "$as_me: creating cache $cache_file" >&6;} >$cache_file 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,) { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&5 $as_echo "$as_me: error: \`$ac_var' was set to \`$ac_old_val' in the previous run" >&2;} ac_cache_corrupted=: ;; ,set) { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' was not set in the previous run" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: error: \`$ac_var' has changed since the previous run:" >&5 $as_echo "$as_me: error: \`$ac_var' has changed since the previous run:" >&2;} ac_cache_corrupted=: else { $as_echo "$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&5 $as_echo "$as_me: warning: ignoring whitespace changes in \`$ac_var' since the previous run:" >&2;} eval $ac_var=\$ac_old_val fi { $as_echo "$as_me:${as_lineno-$LINENO}: former value: \`$ac_old_val'" >&5 $as_echo "$as_me: former value: \`$ac_old_val'" >&2;} { $as_echo "$as_me:${as_lineno-$LINENO}: current value: \`$ac_new_val'" >&5 $as_echo "$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=`$as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} { $as_echo "$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build" >&5 $as_echo "$as_me: error: changes in the environment can compromise the build" >&2;} as_fn_error $? "run \`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 { $as_echo "$as_me:${as_lineno-$LINENO}: loading cache $cache_file" >&5 $as_echo "$as_me: loading cache $cache_file" >&6;} case $cache_file in [\\/]* | ?:[\\/]* ) . "$cache_file";; *) . "./$cache_file";; esac fi else { $as_echo "$as_me:${as_lineno-$LINENO}: creating cache $cache_file" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_SED+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $SED" >&5 $as_echo "$SED" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$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+set}" = set; then : withval=$with_erlang; fi if test -n "$ERLC"; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for erlc" >&5 $as_echo_n "checking for erlc... " >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERLC" >&5 $as_echo "$ERLC" >&6; } else 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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ERLC+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERLC" >&5 $as_echo "$ERLC" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ac_pt_ERLC+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERLC" >&5 $as_echo "$ac_pt_ERLC" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi if test "x$ac_pt_ERLC" = x; then ERLC="erlc" else case $cross_compiling:$ac_tool_warned in yes:) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 $as_echo "$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 fi if test -n "$ERL"; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for erl" >&5 $as_echo_n "checking for erl... " >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERL" >&5 $as_echo "$ERL" >&6; } else 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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ERL+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERL" >&5 $as_echo "$ERL" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ac_pt_ERL+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERL" >&5 $as_echo "$ac_pt_ERL" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi if test "x$ac_pt_ERL" = x; then ERL="erl" else case $cross_compiling:$ac_tool_warned in yes:) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 $as_echo "$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 fi # Extract the first word of "dialyzer", so it can be a program name with args. set dummy dialyzer; ac_word=$2 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_DIALYZER+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $DIALYZER" >&5 $as_echo "$DIALYZER" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi if test "x$prefix" = xNONE; then $as_echo_n "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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ac_prefix_program+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_prefix_program" >&5 $as_echo "$ac_prefix_program" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "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 || $as_echo 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 || $as_echo 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 "$ERLC"; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for erlc" >&5 $as_echo_n "checking for erlc... " >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERLC" >&5 $as_echo "$ERLC" >&6; } else 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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ERLC+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERLC" >&5 $as_echo "$ERLC" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ac_pt_ERLC+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERLC" >&5 $as_echo "$ac_pt_ERLC" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi if test "x$ac_pt_ERLC" = x; then ERLC="not found" else case $cross_compiling:$ac_tool_warned in yes:) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 $as_echo "$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 fi if test "$ERLC" = "not found"; then as_fn_error $? "Erlang/OTP compiler (erlc) not found but required" "$LINENO" 5 fi if test -n "$ERL"; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for erl" >&5 $as_echo_n "checking for erl... " >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERL" >&5 $as_echo "$ERL" >&6; } else 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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ERL+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ERL" >&5 $as_echo "$ERL" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 $as_echo_n "checking for $ac_word... " >&6; } if ${ac_cv_path_ac_pt_ERL+:} false; then : $as_echo_n "(cached) " >&6 else 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 test -z "$as_dir" && as_dir=. 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" $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_pt_ERL" >&5 $as_echo "$ac_pt_ERL" >&6; } else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } fi if test "x$ac_pt_ERL" = x; then ERL="not found" else case $cross_compiling:$ac_tool_warned in yes:) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 $as_echo "$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 fi if test "$ERL" = "not found"; then as_fn_error $? "Erlang/OTP interpreter (erl) not found but required" "$LINENO" 5 fi { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP root directory" >&5 $as_echo_n "checking for Erlang/OTP root directory... " >&6; } if ${ac_cv_erlang_root_dir+:} false; then : $as_echo_n "(cached) " >&6 else ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> RootDir = code:root_dir(), file:write_file("conftest.out", RootDir), ReturnValue = 0, halt(ReturnValue) . _ACEOF if ac_fn_erl_try_run "$LINENO"; then : ac_cv_erlang_root_dir=`cat conftest.out` rm -f conftest.out else rm -f conftest.out { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "test Erlang program execution failed See \`config.log' for more details" "$LINENO" 5; } fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_root_dir" >&5 $as_echo "$ac_cv_erlang_root_dir" >&6; } ERLANG_ROOT_DIR=$ac_cv_erlang_root_dir { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'xmerl' library subdirectory" >&5 $as_echo_n "checking for Erlang/OTP 'xmerl' library subdirectory... " >&6; } if ${ac_cv_erlang_lib_dir_xmerl+:} false; then : $as_echo_n "(cached) " >&6 else ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> ReturnValue = case code:lib_dir("xmerl") of {error, bad_name} -> file:write_file("conftest.out", "not found\n"), 1; LibDir -> file:write_file("conftest.out", LibDir), 0 end, halt(ReturnValue) . _ACEOF if ac_fn_erl_try_run "$LINENO"; then : ac_cv_erlang_lib_dir_xmerl=`cat conftest.out` rm -f conftest.out else if test ! -f conftest.out; then { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "test Erlang program execution failed See \`config.log' for more details" "$LINENO" 5; } else ac_cv_erlang_lib_dir_xmerl="not found" rm -f conftest.out fi fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_xmerl" >&5 $as_echo "$ac_cv_erlang_lib_dir_xmerl" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'xmerl' library version" >&5 $as_echo_n "checking for Erlang/OTP 'xmerl' library version... " >&6; } if ${ac_cv_erlang_lib_ver_xmerl+:} false; then : $as_echo_n "(cached) " >&6 else if test "$ac_cv_erlang_lib_dir_xmerl" = "not found"; then : ac_cv_erlang_lib_ver_xmerl="not found" else ac_cv_erlang_lib_ver_xmerl=`$as_echo "$ac_cv_erlang_lib_dir_xmerl" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_xmerl" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'ssl' library subdirectory" >&5 $as_echo_n "checking for Erlang/OTP 'ssl' library subdirectory... " >&6; } if ${ac_cv_erlang_lib_dir_ssl+:} false; then : $as_echo_n "(cached) " >&6 else ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> ReturnValue = case code:lib_dir("ssl") of {error, bad_name} -> file:write_file("conftest.out", "not found\n"), 1; LibDir -> file:write_file("conftest.out", LibDir), 0 end, halt(ReturnValue) . _ACEOF if ac_fn_erl_try_run "$LINENO"; then : ac_cv_erlang_lib_dir_ssl=`cat conftest.out` rm -f conftest.out else if test ! -f conftest.out; then { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "test Erlang program execution failed See \`config.log' for more details" "$LINENO" 5; } else ac_cv_erlang_lib_dir_ssl="not found" rm -f conftest.out fi fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_ssl" >&5 $as_echo "$ac_cv_erlang_lib_dir_ssl" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'ssl' library version" >&5 $as_echo_n "checking for Erlang/OTP 'ssl' library version... " >&6; } if ${ac_cv_erlang_lib_ver_ssl+:} false; then : $as_echo_n "(cached) " >&6 else if test "$ac_cv_erlang_lib_dir_ssl" = "not found"; then : ac_cv_erlang_lib_ver_ssl="not found" else ac_cv_erlang_lib_ver_ssl=`$as_echo "$ac_cv_erlang_lib_dir_ssl" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_ssl" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'crypto' library subdirectory" >&5 $as_echo_n "checking for Erlang/OTP 'crypto' library subdirectory... " >&6; } if ${ac_cv_erlang_lib_dir_crypto+:} false; then : $as_echo_n "(cached) " >&6 else ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> ReturnValue = case code:lib_dir("crypto") of {error, bad_name} -> file:write_file("conftest.out", "not found\n"), 1; LibDir -> file:write_file("conftest.out", LibDir), 0 end, halt(ReturnValue) . _ACEOF if ac_fn_erl_try_run "$LINENO"; then : ac_cv_erlang_lib_dir_crypto=`cat conftest.out` rm -f conftest.out else if test ! -f conftest.out; then { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "test Erlang program execution failed See \`config.log' for more details" "$LINENO" 5; } else ac_cv_erlang_lib_dir_crypto="not found" rm -f conftest.out fi fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_crypto" >&5 $as_echo "$ac_cv_erlang_lib_dir_crypto" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'crypto' library version" >&5 $as_echo_n "checking for Erlang/OTP 'crypto' library version... " >&6; } if ${ac_cv_erlang_lib_ver_crypto+:} false; then : $as_echo_n "(cached) " >&6 else if test "$ac_cv_erlang_lib_dir_crypto" = "not found"; then : ac_cv_erlang_lib_ver_crypto="not found" else ac_cv_erlang_lib_ver_crypto=`$as_echo "$ac_cv_erlang_lib_dir_crypto" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_crypto" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'public_key' library subdirectory" >&5 $as_echo_n "checking for Erlang/OTP 'public_key' library subdirectory... " >&6; } if ${ac_cv_erlang_lib_dir_public_key+:} false; then : $as_echo_n "(cached) " >&6 else ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 cat > conftest.$ac_ext <<_ACEOF -module(conftest). -export([start/0]). start() -> ReturnValue = case code:lib_dir("public_key") of {error, bad_name} -> file:write_file("conftest.out", "not found\n"), 1; LibDir -> file:write_file("conftest.out", LibDir), 0 end, halt(ReturnValue) . _ACEOF if ac_fn_erl_try_run "$LINENO"; then : ac_cv_erlang_lib_dir_public_key=`cat conftest.out` rm -f conftest.out else if test ! -f conftest.out; then { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$as_me: error: in \`$ac_pwd':" >&2;} as_fn_error $? "test Erlang program execution failed See \`config.log' for more details" "$LINENO" 5; } else ac_cv_erlang_lib_dir_public_key="not found" rm -f conftest.out fi fi rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ conftest.$ac_objext conftest.beam conftest.$ac_ext fi ac_ext=erl ac_compile='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5' ac_link='$ERLC $ERLCFLAGS -b beam conftest.$ac_ext >&5 && echo "#!/bin/sh" > conftest$ac_exeext && $as_echo "\"$ERL\" -run conftest start -run init stop -noshell" >> conftest$ac_exeext && chmod +x conftest$ac_exeext' fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_dir_public_key" >&5 $as_echo "$ac_cv_erlang_lib_dir_public_key" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking for Erlang/OTP 'public_key' library version" >&5 $as_echo_n "checking for Erlang/OTP 'public_key' library version... " >&6; } if ${ac_cv_erlang_lib_ver_public_key+:} false; then : $as_echo_n "(cached) " >&6 else if test "$ac_cv_erlang_lib_dir_public_key" = "not found"; then : ac_cv_erlang_lib_ver_public_key="not found" else ac_cv_erlang_lib_ver_public_key=`$as_echo "$ac_cv_erlang_lib_dir_public_key" | sed -n -e 's,^.*-\([^/-]*\)$,\1,p'` fi fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_erlang_lib_ver_public_key" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: checking if Erlang/OTP SSL application is running fine" >&5 $as_echo_n "checking if Erlang/OTP SSL application is running fine... " >&6; } if ${erlang_cv_ssl_runnable+:} false; then : $as_echo_n "(cached) " >&6 else erlang_cv_ssl_runnable=no if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 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 if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 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 ERLANG_APPLICATIONS="kernel,stdlib" { $as_echo "$as_me:${as_lineno-$LINENO}: result: WARNING: ssl application is not working properly !!!" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $erlang_cv_ssl_runnable" >&5 $as_echo "$erlang_cv_ssl_runnable" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking if Erlang/OTP crypto application is running fine" >&5 $as_echo_n "checking if Erlang/OTP crypto application is running fine... " >&6; } if ${erlang_cv_crypto_runnable+:} false; then : $as_echo_n "(cached) " >&6 else erlang_cv_crypto_runnable=no if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 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_echo "$as_me:${as_lineno-$LINENO}: result: WARNING: crypto application is not working properly !!!" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $erlang_cv_crypto_runnable" >&5 $as_echo "$erlang_cv_crypto_runnable" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking new time API" >&5 $as_echo_n "checking new time API... " >&6; } if ${erlang_cv_new_time_api+:} false; then : $as_echo_n "(cached) " >&6 else erlang_cv_new_time_api=no if test "$cross_compiling" = yes; then : { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 $as_echo "$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 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_echo "$as_me:${as_lineno-$LINENO}: result: WARNING: new time API not available. use old now() instead" >&5 $as_echo "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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $erlang_cv_new_time_api" >&5 $as_echo "$erlang_cv_new_time_api" >&6; } DTD=tsung-1.0.dtd TEMPLATES_SUBDIR=tsung/templates { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${MAKE-make} sets \$(MAKE)" >&5 $as_echo_n "checking whether ${MAKE-make} sets \$(MAKE)... " >&6; } set x ${MAKE-make} ac_make=`$as_echo "$2" | sed 's/+/p/g; s/[^a-zA-Z0-9_]/_/g'` if eval \${ac_cv_prog_make_${ac_make}_set+:} false; then : $as_echo_n "(cached) " >&6 else 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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 $as_echo "yes" >&6; } SET_MAKE= else { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 $as_echo "no" >&6; } SET_MAKE="MAKE=${MAKE-make}" fi ac_aux_dir= for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do if test -f "$ac_dir/install-sh"; then ac_aux_dir=$ac_dir ac_install_sh="$ac_aux_dir/install-sh -c" break elif test -f "$ac_dir/install.sh"; then ac_aux_dir=$ac_dir ac_install_sh="$ac_aux_dir/install.sh -c" break elif test -f "$ac_dir/shtool"; then ac_aux_dir=$ac_dir ac_install_sh="$ac_aux_dir/shtool install -c" break fi done if test -z "$ac_aux_dir"; then as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$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. ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. # 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. { $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 $as_echo_n "checking for a BSD-compatible install... " >&6; } if test -z "$INSTALL"; then if ${ac_cv_path_install+:} false; then : $as_echo_n "(cached) " >&6 else as_save_IFS=$IFS; IFS=$PATH_SEPARATOR for as_dir in $PATH do IFS=$as_save_IFS test -z "$as_dir" && as_dir=. # Account for people who put trailing slashes in PATH elements. 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+set}" = set; 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 { $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: Storing library files in $EXPANDED_LIBDIR" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: Storing data files in $EXPANDED_SHAREDIR" >&5 $as_echo "$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_*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: cache variable $ac_var contains a newline" >&5 $as_echo "$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+set}" = set || &/ 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 { $as_echo "$as_me:${as_lineno-$LINENO}: updating cache $cache_file" >&5 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: not updating unwritable cache $cache_file" >&5 $as_echo "$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=`$as_echo "$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" { $as_echo "$as_me:${as_lineno-$LINENO}: creating $CONFIG_STATUS" >&5 $as_echo "$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 if test -n "${ZSH_VERSION+set}" && (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 case `(set -o) 2>/dev/null` in #( *posix*) : set -o posix ;; #( *) : ;; esac fi as_nl=' ' export as_nl # Printing a long string crashes Solaris 7 /usr/bin/printf. as_echo='\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\' as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo as_echo=$as_echo$as_echo$as_echo$as_echo$as_echo$as_echo # Prefer a ksh shell builtin over an external printf program on Solaris, # but without wasting forks for bash or zsh. if test -z "$BASH_VERSION$ZSH_VERSION" \ && (test "X`print -r -- $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='print -r --' as_echo_n='print -rn --' elif (test "X`printf %s $as_echo`" = "X$as_echo") 2>/dev/null; then as_echo='printf %s\n' as_echo_n='printf %s' else if test "X`(/usr/ucb/echo -n -n $as_echo) 2>/dev/null`" = "X-n $as_echo"; then as_echo_body='eval /usr/ucb/echo -n "$1$as_nl"' as_echo_n='/usr/ucb/echo -n' else as_echo_body='eval expr "X$1" : "X\\(.*\\)"' as_echo_n_body='eval arg=$1; case $arg in #( *"$as_nl"*) expr "X$arg" : "X\\(.*\\)$as_nl"; arg=`expr "X$arg" : ".*$as_nl\\(.*\\)"`;; esac; expr "X$arg" : "X\\(.*\\)" | tr -d "$as_nl" ' export as_echo_n_body as_echo_n='sh -c $as_echo_n_body as_echo' fi export as_echo_body as_echo='sh -c $as_echo_body as_echo' fi # The user is always right. if test "${PATH_SEPARATOR+set}" != set; 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 # IFS # We need space, tab and new line, in precisely that order. Quoting is # there to prevent editors from complaining about space-tab. # (If _AS_PATH_WALK were called with IFS unset, it would disable word # splitting by setting IFS to empty value.) IFS=" "" $as_nl" # 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 test -z "$as_dir" && as_dir=. 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 $as_echo "$as_myself: error: cannot find myself; rerun with an absolute file name" >&2 exit 1 fi # Unset variables that we do not need and which cause bugs (e.g. in # pre-3.0 UWIN ksh). But do not cause bugs in bash 2.01; the "|| exit 1" # suppresses any "Segmentation fault" message there. '((' could # trigger a bug in pdksh 5.2.14. for as_var in BASH_ENV ENV MAIL MAILPATH do eval test x\${$as_var+set} = xset \ && ( (unset $as_var) || exit 1) >/dev/null 2>&1 && unset $as_var || : done PS1='$ ' PS2='> ' PS4='+ ' # NLS nuisances. LC_ALL=C export LC_ALL LANGUAGE=C export LANGUAGE # CDPATH. (unset CDPATH) >/dev/null 2>&1 && unset CDPATH # 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 $as_echo "$as_me:${as_lineno-$LINENO}: error: $2" >&$4 fi $as_echo "$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_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_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 || $as_echo 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 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 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=`$as_echo "$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 || $as_echo 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.7.0, which was generated by GNU Autoconf 2.69. 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 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ tsung config.status 1.7.0 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" Copyright (C) 2012 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 ) $as_echo "$ac_cs_version"; exit ;; --config | --confi | --conf | --con | --co | --c ) $as_echo "$ac_cs_config"; exit ;; --debug | --debu | --deb | --de | --d | -d ) debug=: ;; --file | --fil | --fi | --f ) $ac_shift case $ac_optarg in *\'*) ac_optarg=`$as_echo "$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 ) $as_echo "$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 \$as_echo "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 $as_echo "$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+set}" = set || 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=`$as_echo "$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 '` $as_echo "$*" | sed 's|^[^:]*/||;s|:[^:]*/|, |g' `' by configure.' if test x"$ac_file" != x-; then configure_input="$ac_file. $configure_input" { $as_echo "$as_me:${as_lineno-$LINENO}: creating $ac_file" >&5 $as_echo "$as_me: creating $ac_file" >&6;} fi # Neutralize special characters interpreted by sed in replacement strings. case $configure_input in #( *\&* | *\|* | *\\* ) ac_sed_conf_input=`$as_echo "$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 || $as_echo 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=/`$as_echo "$ac_dir" | sed 's|^\.[\\/]||'` # A ".." for each directory in $ac_dir_suffix. ac_top_builddir_sub=`$as_echo "$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@*) { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: $ac_file_inputs seems to ignore the --datarootdir setting" >&5 $as_echo "$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"; } && { $as_echo "$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 $as_echo "$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 { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: unrecognized options: $ac_unrecognized_opts" >&5 $as_echo "$as_me: WARNING: unrecognized options: $ac_unrecognized_opts" >&2;} fi tsung-1.7.0/CHANGELOG.md0000644000201100017670000010245613151315546014171 0ustar nniclausdream# Changelog # ## [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 occured( 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 abilty 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 costum 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 overriden ### 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 assumtion 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 sesssion 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 realease ## ### 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.7.0/CONTRIBUTORS0000644000201100017670000000305713151315546014235 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.7.0/debian/0000755000201100017670000000000013151461561013571 5ustar nniclausdreamtsung-1.7.0/debian/rules0000755000201100017670000000237013151315546014654 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 # This is the debhelper compatibility version to use. export DH_COMPAT=4 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.7.0/debian/tsung.dirs0000644000201100017670000000006113151315546015612 0ustar nniclausdreamusr/bin/ usr/lib/tsung usr/share/tsung/templates tsung-1.7.0/debian/docs0000644000201100017670000000010013151315546014434 0ustar nniclausdreamCHANGELOG.md docs/_build/singlehtml CONTRIBUTORS README.md TODO tsung-1.7.0/debian/copyright0000644000201100017670000000143113151315546015524 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.7.0/debian/compat0000644000201100017670000000000213151315546014770 0ustar nniclausdream4 tsung-1.7.0/debian/control0000644000201100017670000000203613151315546015176 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.7.0/debian/changelog0000644000201100017670000001141013151315546015441 0ustar nniclausdreamtsung (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.7.0/vsn.mk0000644000201100017670000000000613151315546013503 0ustar nniclausdream1.7.0 tsung-1.7.0/tsung.sh.in0000755000201100017670000002014213151315546014453 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.7.0/examples/0000755000201100017670000000000013151461561014165 5ustar nniclausdreamtsung-1.7.0/examples/http_simple.xml.in0000644000201100017670000000424013151315546017645 0ustar nniclausdream tsung-1.7.0/examples/http_setdynvars.xml.in0000644000201100017670000000564613151315546020571 0ustar nniclausdream tsung-1.7.0/examples/pgsql.xml.in0000644000201100017670000000344213151315546016446 0ustar nniclausdream SELECT * from accounts; SELECT * from users; tsung-1.7.0/examples/jabber_muc.xml.in0000644000201100017670000000607513151315546017416 0ustar nniclausdream tsung-1.7.0/examples/thinks2.xml.in0000644000201100017670000000226413151315546016703 0ustar nniclausdream tsung-1.7.0/examples/mqtt.xml.in0000644000201100017670000000361413151315546016306 0ustar nniclausdream test_message tsung-1.7.0/examples/websocket.xml.in0000644000201100017670000000237213151315546017307 0ustar nniclausdream {"user":"user", "password":"password"} ok {"uid":"%%_uid%%", "data":"data"} {"key":"value"} tsung-1.7.0/examples/mysql.xml.in0000644000201100017670000000216513151315546016466 0ustar nniclausdream SHOW TABLES SELECT * FROM gens SELECT * FROM te tsung-1.7.0/examples/jabber.xml.in0000644000201100017670000001017213151315546016543 0ustar nniclausdream tsung-1.7.0/examples/http-digest.xml.in0000644000201100017670000000414213151315546017552 0ustar nniclausdream tsung-1.7.0/examples/amqp.xml.in0000644000201100017670000001075613151315546016264 0ustar nniclausdream tsung-1.7.0/examples/jabber_node.xml.in0000644000201100017670000000676513151315546017565 0ustar nniclausdream tsung-1.7.0/examples/thinks.xml.in0000644000201100017670000000227613151315546016624 0ustar nniclausdream tsung-1.7.0/examples/fs-nfs.xml.in0000644000201100017670000000736313151315546016522 0ustar nniclausdream tsung-1.7.0/examples/raw.xml.in0000644000201100017670000000174013151315546016110 0ustar nniclausdream tsung-1.7.0/examples/ldap.xml.in0000644000201100017670000000470313151315546016241 0ustar nniclausdream organizationalPerson inetOrgPerson person %%_new_user_cn%% fffs SomeSN some@mail.com tsung-1.7.0/examples/jabber_roster.xml.in0000644000201100017670000000530313151315546020141 0ustar nniclausdream tsung-1.7.0/examples/http_distributed.xml.in0000644000201100017670000001436013151315546020702 0ustar nniclausdream tsung-1.7.0/examples/bosh.xml.in0000644000201100017670000000503413151315546016252 0ustar nniclausdream tsung-1.7.0/examples/jabber_register.xml.in0000644000201100017670000000245513151315546020454 0ustar nniclausdream error tsung-1.7.0/examples/http-oauth.xml.in0000644000201100017670000000472313151315546017420 0ustar nniclausdream tsung-1.7.0/examples/jabber_privacy.xml.in0000644000201100017670000000317613151315546020306 0ustar nniclausdream tsung-1.7.0/examples/jabber_starttls.xml.in0000644000201100017670000000527113151315546020507 0ustar nniclausdream tsung-1.7.0/examples/http_tag.xml.in0000644000201100017670000000241113151315546017125 0ustar nniclausdream tsung-1.7.0/TODO0000644000201100017670000000006113151315546013035 0ustar nniclausdreamSee https://support.process-one.net/browse/TSUN tsung-1.7.0/LISEZMOI0000644000201100017670000000540613151315546013533 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.7.0/README.md0000644000201100017670000000147713151315546013640 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.7.0/COPYING0000644000201100017670000004325413151315546013413 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.7.0/tsung-1.0.dtd0000644000201100017670000003466013151315546014512 0ustar nniclausdream tsung-1.7.0/docs/0000755000201100017670000000000013151461561013277 5ustar nniclausdreamtsung-1.7.0/docs/images/0000755000201100017670000000000013151461561014544 5ustar nniclausdreamtsung-1.7.0/docs/images/logo_tsung.png0000644000201100017670000005222713151315546017443 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.7.0/docs/images/ldap-hierarchy.png0000644000201100017670000013363313151315546020160 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.7.0/docs/images/tsung-report.png0000644000201100017670000024314613151315546017736 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 آێ-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.7.0/docs/images/ldap-results.png0000644000201100017670000002505213151315546017676 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.7.0/docs/images/tsung-graph.png0000644000201100017670000010400013151315546017505 0ustar nniclausdreamPNG  IHDRCbKGD pHYs:duhtIME 9tEXtCommentCreated with The GIMPd%n IDATx{eOt&p  9슮n Zey%,]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.7.0/docs/images/connected.png0000644000201100017670000027642713151315546017237 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.7.0/docs/_build/0000755000201100017670000000000013151461561014535 5ustar nniclausdreamtsung-1.7.0/docs/_build/latex/0000755000201100017670000000000013151461561015652 5ustar nniclausdreamtsung-1.7.0/docs/_build/latex/Tsung.pdf0000644000201100017670000231712113151461561017454 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 1376 /Filter /FlateDecode >> stream xXO9~߿ֿmޝし4l!P6o"Ŏ j$ogW&[fo9q΄bB 5R3614Ό獐*abe-y̖OL*ZHhu 1YzE.\)$EOJǯ 5<-TjNÉ(5 C EH`Yi7;b H'Hg|"ˀS ] (NAL\8\Hi$pp BUAr L)OCJyC)ģ9 N(m8Ғi;ƴL+O]ҴkR6m Pnl8N{/pgZy-e. kI[#0h"* qtǎNث~{yu o/۶ČĂĜ+$atFg#}7^Lf > ml!o}r,OݠDn8|9 \5jx;!J 2uSCCgbHrbR C%xJIM@lQydZۧ4V5'i{`)M^Y,{-UHQ;溦Mɸ6rdPw6pvq>#c*[&(sp5 b[[.wmujՖHM!d9S2 O;Ȉ)2pXVX\6(sw o2@Fŝg~R* y( whKw6gZcQY:6˷Sƾn&WNӠo0qlu\!GT4=*`c[iy5Juz (}ͰκJ[x 3JOa?Mc >-2Oonnb]1v4Q kUmR4;Yێ}Zmg5E}>Z]XoL5d_3m/V3g b'F0x/p,u^3 o!Kqr|PX; endstream endobj 342 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 344 0 obj << /Length 240 /Filter /FlateDecode >> stream xڵj0EYPOfֲȢVq~q(hIG xQX!hL茇W=A[ LdDC thxQs|]y8c3pϧq(y+|³3Ȃtck\mS*CΡ1fe_OʦTt<Ӌ#$).ӽ_ b>ݺa1|SV?d endstream endobj 351 0 obj << /Length 19 /Filter /FlateDecode >> stream x3PHW0Pp2Ac( endstream endobj 391 0 obj << /Length 1144 /Filter /FlateDecode >> stream xMsHs|sڲ;YW*oX"e)sASznA0@s< 'zr /rA)` 7| ?Wt= }="O}2uԽғCս"#o\B\;rTT?̬TxP}KBAR'3+ʵOӰpXŹ>2!!S*el|H`QlɌ8<Β00듻˜oS<{._,~d&NRah< i=i„ "|M>q1sX4<UoևzZ,S㱫5\0kc;Hp\3ˢ$,rj\%꟎޺:z"cb{6+6^aixse;Έeڷt>ϋI]]|h3Pq{;JpZ11Ur}D7|t"uVG ^7.D-"lgrɋ heYr{!W'C_!V٢$~!M2ts4ET%'ḛff>[$QUbP*WHmDǓi yc˼?\>jߤ}Q:Q)X"kc=٘H2H]/%?fPe O  ēe&&V0Ն4Fğq2ZOh\N!lt:Qh: [q:,G =t<*mCO4PuuV6Lz XluVƚa׮% 3daqz9u'2Zv9,rѣJKRy`kj16KQX^I-n͘vWK~h81rT6k7Yu0*Gi֭%!6c_rL$qhR 0`jן!˧%Z郻՛fr$jpQޛ&rkړ~> endstream endobj 204 0 obj << /Type /ObjStm /N 100 /First 885 /Length 2238 /Filter /FlateDecode >> stream xڵZr8}W`fj!/[Le2M&8yP$V,y$y!)VQAytTB3L!(IEGUeJ}8W9ߕf^R10)!4EV;&q3:2DDR¸2U8 A"xYЙK8ZQ'C4`ѱaaDvDviqK={ @dqB !T#cpe:hTCCc!et̸j ip@PC Gh=E.0+":bk`)d;AJ#xn-ތ4g!#y#T#34@G(A{RR ALc$:*'IQ\ueYؔܥ=\D\ёVD!^P\/ӃH[ϼ){t"^ !wAs R I;Ai5Xp1 ˎ4tdEBDt waH ),ZcYtR낚[ޒi>x:6_X+jRȨ3j;>?o-0"duMƚ-0|c._Q3I/-3ymGX3e&:_o7͢ۂ@W;ٙV9VNTS}v: Tԃ3Zm7`9˿𻓛zĥ>S24ƔLԫatf?uJbl[AM@'jҩ) r-m;*ǭ2j]/ic_yyߟ)kL4M#{9{ʦ=p{mICa7z>w-K^-KyoW% oMuM>\9n! l7Y=gEJ۝a4 +È cytvr\|D IG.}:tG'Qcs %<\s %<| '<| '<| /$B /$B x{(u?ٖΗ?ֳb]J.>_ߌ_]4E\BfuJJ8&p5^šGdU[EzI/7.ȵkPHRr'PHaȍvBdzxyh#RCHՠ{xy@R7 F \vx"`57FEnp:<8g=Ap<>=< 2 O0<E ^ 7m<O/Tx"}^Ҷ }˩OkX}o1{j,na8)B j9' e/47{+$E-EtjO*aK- cPX~>^SC'((+g }4_m1(Z<.([{hgJʀ-\OUsd1 endstream endobj 446 0 obj << /Length 1571 /Filter /FlateDecode >> stream x]s8՚ ɒvvml;6%E(OA$iH&")ݘ zX0f0~lBw &> NlVڮ1k@1Ok",.rgUƧ` 5ж=Ӷޞ8}8N+g9bNAH-X>c8ڽpgE U(n۵aYtW,S/> T:)ѷ$ `( A 3xi{F }ɵ D)b^!LI"gE-vsjr@F# h}yTS4NԒ=YJ m=>vֈH<ˮæ%qBj\D-h;ghLkxyhz?ZQgj3QF =b ~3 /kxLSn5_nC;CX'Tg,qH#VP@ٷVKM+v{{xEA١%Poȕǯ5;"gV |3Th&B 9 Y@5}~DMBYJd}䨟}][ĎתGuUcI-Q`z\͠ӌT%_4dU=,V9%SQ _ m`E0!}8BqJRO˵@YlcK:/dϩ*:i&hb-?$CTrG>I%+iH ,aqIJ\ %qWx-~dMfG*B8Jp/47 t䬖n3V]N$eN -Q̄ˣ'sMEפquX# SiCY^6[QJN 4$Z[ t!AY(\УR3qRPEPTɗb+_8f+gp[ _0޶rVWst%cb8 k65}xwrӻeDQTtξ|3mS٢{u .2!u!wa ڬC&l-+"P{Dpe$tB=`]PPsi[WwB8exa~ɟZ=_A5YU"xv_tKp>p)߰s7Jޯk{΢M.(+x!4'mn55pEX^FߦNBt3Xժ-n/ 1I_Y"R/V"~m&~m{6}بc-SFeUՌe;Ao_ָjWB9߾M-N͇A=(GuE HmZ=6q-fH;6w5Wj՜d3/]mFޟ-ZƲ7Y} wQ\`}dp=lzF>}F$~[,I]W#1.?A~ lq 1=% endstream endobj 450 0 obj << /Length 164 /Filter /FlateDecode >> stream xe; @SjsZAB*c$`m{IP|4̰N ^[* [6]!I,V#Cj<$Tͥ9>u*al}~٫$I5R9C/d(b} 0.sጽN@:|YEQYM]v>d6 endstream endobj 454 0 obj << /Length 113 /Filter /FlateDecode >> stream x3PHW0Pp2@ Br.WtB PK@B(WH(sr9p[(XY)01344RIQ,ӌ [9 endstream endobj 461 0 obj << /Length 694 /Filter /FlateDecode >> stream xڥUmO0_ᏉD\;~I0&16C"M6Y㔒ұ1>wy9@CpD %O8fpS H%ìiy'tѮTerSjR)Devg{ uGR`|OD pE®Kp| P_#ؖ`‰.4V "r|((7r >JX[oTJMh^W:,V>ѭ[ϊCPֺR&(=2TȞ. %e2;jyLM -U[G͡r%&_s,"QL׫*NTU,Vy}Y. n]ͭץnojmtYVŲ>fY1tQ |!8$lEO:v"B5iJvG䜮c0NɔHC!V`˔qCU/j[N~4вE[ V{$y v{Y. C+Kh'[ЫXѕ?q f4!&񰝛:/x ND&\W͋ cn/pGeӴt5c&`h}9͎O ǯLFxKo endstream endobj 468 0 obj << /Length 190 /Filter /FlateDecode >> stream xڍ @{)tݻw_H׉$հ|x1ZD1F3Վt|ՔynX_n5;VY7HO܋Cv,[ ?0'`bGU@~WX2G$ԊTz)65땟POɺE endstream endobj 475 0 obj << /Length 2218 /Filter /FlateDecode >> stream xڵX[s۶~ۡf"Mbq'q&Vt>@,! FbZU<\ `owz/ޜ3c<, 8w^y W%wu Ϣ[>xI΂Eʼ,r}_=]^MX3, $̓0c41b@ֳP,8VnK0A8 uyD͵ة& * ,1“ɻ7g4Nx4yf6< >,y[;Zf֮:m ̓-ox !. P1^C<-h.ׅxmj@N1q["VT a mvdn8 {~K4-%*nϷpS_e;2% I' )mp>T}K4QY*XMWkY @hUW0IN\w A%G5BXj*E 25' _D&qTצ0{^;@;L^AF Z[0XQD-$ȷ8Դ`p#FwQSq%`Y2/Z^v7y` "4DNmy\B5tqrYMJ讕v98+ȼߪҒPt'xۊ7_,r48+0_Zq9c bX &qbAUxFܷzlO sd5[wLrrWN c_6)_ھC| =C+EHKIt&7^Ҡռ+,}Ŀ:kMCSC7 CFc'ivr& +QTy 謋 aP1U'} /_]@u\k*{:LKRL46-"mam-P!l\Q %Z=mUE:ȩԏ `*NӺ!nۖhg~?~iLn>Ooty /xD!3L>wE%Rd_J{ʶ@CX;.~Š?E[S%d0.&!LnUKhB<`SA|ekipi$:vA`_VV65L"!:tt? z=9r}5UB Ra޵pOuΠI0Br1ad UFDJU˛M7w#7uRwZJFPX+`mS!oklF<ѣdyG}vbzQk"!ƀ?ot䖵nvЍyt}Nw譒hI; v ]V;)n3s@e? <> Dc] endstream endobj 482 0 obj << /Length 1620 /Filter /FlateDecode >> stream xڵXIWH+t'Ar/Z^ $3rh V"KNK !~Zg*RWK8wqNMF' % _OܗEJ{i%E^ij̄=REYOG8O4H2!8^Jr7N{RmkU.O%(;)o/\9ŗkL3]j h^^uaH0ǧ4Ȣg+"W> ^{R.(ci:+c>Wy[OAeBw)FA'w`%2 <-o6#Bɼh<@ysaeTRe*eV6ܢ%.wȴ:qkF3{r< 2:VBsXΠLb&65Sh` 1Dn3i(W+p޶˃^9gA dNݻѶ+{x'^ws=SM1`xj Ѳo!h#?a=!U+U%[|eYs?М"#&[mpO0Tzd_W >5h?J0G`M;R{4Flg=Y5xxQP>s^'4e_Euh#QLFh)]يJ֝(<@w˯ MxyAF5ή|B{XM:x)~zcqa*EuUU_p~^/];"G%%ŗo0l*&G>nx|.+,]U~#fc^-]6\bZEU΁Bd2T4-I,ÀA~ Hm$= C,TG+ B2P#ܠ VL#CAS`!ZnkZ r2(˺I; O|E1]X} 4h8݁a̹ XsVjyB#aՒVh~bQkQ;Uܡ;+쏰b) WEuǧgT-Džeh NoxD+Dpfj^,7<F33 |TªUZUѴj;0( }iGbiT4>2ǝNv5zt3Ph;rB;9з.70ns5"AƩ/k o٭[l{= / u71di: qgn RoC:5MN%PE(MS%+C%0-. "/ek\ Sur9lb Ahت0X6]πooCqm^oWKl6o9A=ل,chQIcz˨cF&Y mKrs endstream endobj 488 0 obj << /Length 2307 /Filter /FlateDecode >> stream xڵْ6}o!,}x<ŒٲSIXS(_ tCj9&G}_Plyu:yaq~ 8_soNŏ?,/ ,ʜI/YV'|yyƀ'YaXf3b C_~)YyUG PN~^^_ze.?^V4We1Y(xՅ3qv1"4K 훭73hㅁY+ޞEeTf~E_$ (3cu~aov0w^F=k;AȞ!||MARX5k+}:3]5 W*7#k[\[hxЇVYWw\}͒$q_w)jY&eJnɃ#v5M5EQYf/O+mqb4x';De*;q$ZȎv$l /Jݯ^fA`EUJ44?m0G͗NJBHABVjkbRwũX%:!(B^[AamTUqU3чvB ג}S)^ѭZ7鴀b5s=tmXh+MSY5Tᶵ[ g,#~F4v|Y<[QH @+( :ì ̚muS?ׯњ7 L|ߋn#xS1EV3$MT]faH8Bq4p!CGEgC(.sG%>&./~|KQu(+A8|~Nu&-ѐ)s{fR`р T<`_לPOSA4 *`1Y|mZUʋs-?gE  c{,]XobJe-96LGOK^6J YpDT% Ku<1͆w&;HGV5cohBU#aA@eC[e y7[A Yќn{48[ι亰LF?*O'w^-M#. ⪠/ykN61k>,m0^aHjm9g1]KUY`Spa`iM}>#%P~DZgj|{:{01;ǹ ^Բ=ݯ5GudYqt> gĪ&"֦~Y`ukCTĶ &Z XJQ($lEZL} &;Ntݎ軵 G>plϺCQvdդ(*V|Ǿ c?guP렖?4r RAGA43.DZC9FRo/zƩ%4I^$ 0+-^IИ4@M +aՀ;e{:zgO!*= :縪.D6WpPRW&#[C_5:P-vLF(J(| *om;={Kk.$O٤%Irx|C¢m1-}лTV:)z]Gl5S#7[rb+QxAG"l`MD->`΢/z3}Fʺ]8B<ǐi9D#u~L?gWo4|Ha._9> stream xڽ[mo_r!Y8I8O $r}fM_82u@ݕsv\E:b .ő$|_i ur5a|G )bɐ%FG6DK4|R}Uc pXUf)R`5F`\P\ 8)@݅eKڃ 8ʳd_Sb}쒈}ZqAha!U[Pmpuwpp3MMUt='9&iG:%{k8Mj_U0Qg)Ӓ!'!WeJ 2cf4ۓSTE!9fՠBD\]^7`j ˜1%TW'0Q{T\PR:"vpb6&j4ɘ)_ 0V@`I΀ rY6BGCw,,sf ;Vh(3ASd❙6L&g@A BDT쎬f5@3L8lxs7.l8|;_2,W ^ O_x1*xÖS*Q n 7kw\uAx5r>q = M{R%\a 3G!py!\F`ɇ|ŵ68`Oo*C)=!plrX+)z#"bGOGG%[W RrvGۊG"OKBe+ #->ũrby`xh 25'?vbZ^mz.u Hp~XFA)8t j`:SBp>S8wXTIѝACYAOAUu G`!j88WI ,Dm>!S j2ѝADrб t϶^==0fQF$T Cx͓*}m:F;D=4j{cdGU_ B [h{Z'{? l=КVZǵd[ζ'@lMwZ%ڿjdD =*ؘ%zۑ`0oocoփfuBM}^֢ɮ;j>}k۵&vGQױsЄ`\ ]?m0ǯŋ&uخYޞpF X(y=]5-RyA߸x$P' \eo/o_O>xuO~<:solh̩aaw7mͷ#ˮX;;:)A0b۽aeD^&VNޘmC4;T$] }fb>/ad~|w1||x|t?qQu  p`v?jOdgdEz0l/JBI&e$ntn endstream endobj 497 0 obj << /Length 1417 /Filter /FlateDecode >> stream xڽXIwHWp, om"ag%9 hK Ze~T pq覫jAH[iH]W|wK4L˧Zh+O\On*Rɵ9XT2MDƇu/{{T K"W7wk [rUF7Aǯ xu^9׫`* kA@ǁ K ·"N^ HQ$WeWxӧw RlR:Z.Yl#u?RAJ30UGV+j6Ҽ_+oߗ/7dS@ hqROނ,YGaQZe <Ѫ㜗j#tOߌa j4;2i޷լ~7_Fف~Wy#L-rBbOv9]J)ɋKNPhSuV',"!sn^sa\얧1rU)4GӚ7IeAF|bz">(> sYHZEYSg췣r)J UoFs4Ԃ-o:}/z [1N!8aIGT۠d6ٚ:z0g ![ Nشw|0vU/?t*-ԋ)80c8*{l^H`pѯG@/Z endstream endobj 502 0 obj << /Length 1085 /Filter /FlateDecode >> stream xXKo6Whk[-M,nEZbl!zEͿP$e[fS)zgfA+DoO Q#9j 9hi5p|Qm DZ 8kZP`b}[|=(!@tq@o%D!zʑh6"c)@$/6o6^Q;oKy` G6;AJy^ ŽEɘ3ּٱ4ܭx4C>uڀ]7^:Rak’߅:~6A?]ٳ>HZI.Ken jT HNWk^[7;> c< ζݧT< hgX4r.e2|G<=H8F⼌kXAf^!&&6 NðyZ6eldam9ݳMf|&UԴUUB[6N$] CplaaC 6|r VZKb|))kmdФ:?i/xKBsš9ԟvy2KxY+b@2Qq]L6Jՠۤ`$H]$Zj*w!+ LI1Xj /MGtE?,,*HF"˫V!nfydb_c nPVh0h]L}1gO>ξ̮O0 ӦC 8!82gXRQ;&?Vo%fT2K)qY4pM:jJ]+J:YM1f=HL#D׬b?erWOӓ5oxeQXs^=RuzxW,+9Wxh,VcFvlM\K>m6lLUi|+so}oz2(P4tuaxdAmƙBIYO^&dfuxj\ V { Ieode:*Sz>2*Qٯ{O{o !{ •w ~>跱S^ I6 endstream endobj 507 0 obj << /Length 1800 /Filter /FlateDecode >> stream xڭX[4~? qmJ[LKi0= J"j[FOI=+i-!cyu뷟 |{{IZuTnAI2N6xުi8lD3lTs1|WcT1|I27>y|{MWAGfQA߼-Z? (]YQ ^t;⥚yPA28-Q;Br{qwڼ5 q؉v8N `g{=0:9#H3$0StoEiV6)۴F \8[~vlfġV(QL.pkl z=Nth!&捝Qvjݝ*d$Ғsjc.l:ovi]7JA&+SAțX l >J..[am$W;;& .҇M|8<\碏{RFuCƂb$/jMLBbth$"I?l¨F1MIj34KF kL-_Lea#yV3 ѝ4)&q6PZω)/)׃Syf>(-']PF Kꊗ"[ pvUӅsd8ch&)`c)7Yn@(;I(&dH: [䤍hT q"&®4OMM6i 3,/{00ޱ#bW(ey 3wvCݼ->Pwl$^Gf&K=M 1iNTʫY4a*-YT*]+)˟Q␴ yY25K媁wƗBEDqZcXЄx%t>};;]hRAh bka#c&@m/ &HqΜz1oGr2]ZΩ[al(.ih,{OϨAͶikź}#yzwLS"lPﺋBfzv6֥;T:BGt+[ۚVO'Ub]0Oˬ?fe{#>Y9 hgZN=l(yTg_p>)LPZORd~I $n,rB;Lk1>dI]nbaQ0 xC[T:,X٠PnKܬ;I+qs7I8ِ.plZzT-W(\ oEfYs3ʧSP _Z1mpt E# .0 \;]`Y;|!i=o\8.8ӓt&W\ܹG2o 5}y5@nxe0;b|4ߎH¬jk$tWXT51Mt>ap>vuhܤyK` yEl(sd@Y=Z86Fbcf s .Nn< +$vI΄c4-$8@T>1PE('BR= Ml@d?{5b.cXݤm%z{ 'r6wA[Aiwnv襴L<LྞیZgFョI:isj "3Q27<ipQݹp{ۥԥJK55ߙe^^2bvVxl7ʣ#]+7O_ u+ endstream endobj 519 0 obj << /Length 2145 /Filter /FlateDecode >> stream xڵYmo8_OpEROivnhYf,5K7ádVtBrėgc-1JV ;̓.>X;b9,|A:XB[/7dhE,om,7,Gqdz6;vݝLKES͛۫+{qj{ TF˹2`2d.-f9%%M=w==tJf&͚R:]}W46ҭRZմjGK#ΊA]8OR/ׯC\Ǻ:kb}i* Bxv6W$u]ʏ>mݤe%ܱ~Pl&]r"U~c7] ްYq{Wև鷉m!'Tp;+VXAݱSr6hH }!T<Ů5n:XQB}SqL *z33 LB+u\YkNvٵUR0`[>7d:/^۰*%Y 4IU-%HKLmo$$Mg-)5xg0:n8TEl]%Q9}FE )L+¨zlSHh"8 8 8ǚ> P{%lP7$ J ^yJWy'rfT ןg42ޫD 9 #W\C@ӤS> stream xڵY6^ IzmMR$t-r5>rHYe'isY (z7z^]X]4#obߋyBxxX}y{d,;ѩ|{2HL.^/`z Ir~^?yiݛSǣ>s/t /%4oE0YlDBm[7>]*Vx^u;wu#&,I(g0t<>jABw?4U^5@*-z8pO"1.Ns8dV5B,b/Ed)z HGޒ1! ]܀3\fhc|g=Jďmy 8ڼ~>8 8PGhJ{!FI۾1.`#MsJZM< t:VP3G'If`% 9mEQ;@E V}۬pXefe$NH ! vF$ QD@Œ$ ly[b MiAӶB Yi$GΫ 845> ? (&StUhy[|FNV J᪓mUY6}@ MTD($!`@(\JrKfr|V/>,^Ҕ9G =X svu_Z_2`n]^B(->aBm'\<}fۘT/~2i DQ8U˫7n#O٧sN:tLF}wj_ylBi RTMulXCDqFV!5]BL]BM\F (Vrp?<bvgҨD)y>MfE8HçGU=ܱl9/_|FBN,VT3B۪}[trRo ?i4gA[Ktihoh2ˡn PK@2D&ĒHye:JK^޽{bwa+p:p^Ĝ3ë$MX`R$e4 G揎1B`l<LjR0Xo(J]SAVvR_R,q`wKK ym FU'֥8$?8k d6p5.oreʶ^U&gmi:8U7Fkһ B'eՂ(0`Df cZxJSvDT]|yu/|s(gܢ*V90UAOm"a_^9HBN+_ͼrmԫ_^ ϱA康.שԝ->T퐹ݡ=olo(W\<?kkG^|(ۜSZ 5tp?7WYz2Sϩ$4e'y?KYcli6ܛR\j'o.U,;ذ_޺)Fz'T7K2:t7?\+!cLݗjc8g*m]ʉ]m]j]p[WY{YyTŁBE#Nq OĐBaDB88$7$I< bB78ե mqD$H_)ΕQU5·9&Yba.x8! `s`=$}ɼ>볷n<t< p{DH~ӔAxv LXoqv2͒$$ベ&G| NcNNr ?!}=!9Kx[dqG$! h4ECLFŽsܶHovQ7һ榥wG7-?\;Sh&4ta|K-,UY}) b-|ZܩЎI/PO-BN?>ZPA/|OC Y>m^qQ^)8n ''[<IraNPҍ 0h8:FG>@-I9>bD[rnS]LgWx4 cvPӃMw;Q6L/l?P endstream endobj 534 0 obj << /Length 499 /Filter /FlateDecode >> stream xڝSMo W .1mjn:j%k+I n_0l*'{o0IKPSGs@*lam񐮨(W̃?FJ6 Ӈ.V'/ q< |J{U .xJ~&8j|[q1<8; n]rƩ@(GŢ$`*0k;٤çٺ`'u`EKT9 R.,mUx(, ˝m9\q;%cFvot@켄9*lRBE(~$!*PIi?Pgseζ.eFeUf3ƹ? Pfwqqb܁O6/e+QacԨ Iߣu=.6ҫ XQE˵$R?]C endstream endobj 538 0 obj << /Length 207 /Filter /FlateDecode >> stream xڍN0~9&ݮ׮\U=D)HI $юvgcSQ-oM@C /6m *P/ć;i8\1uS4yz&w .<{z2_4VBOAOZY? R|7qWnv^ڷ)}.C‰T8]7Ϙ~ m9M endstream endobj 545 0 obj << /Length 2132 /Filter /FlateDecode >> stream xڽYmo_!K@V&ER/FZ咴&.!]҆캿3J+ie Ԉ3/sgFq/@[>Ƽ{,H{wއήVgDAdʃDq/JÀ3rv9Q@["H9~]zv5.Rq Nd{Ӯ_Xq_^_,BP̿\H[a:s,#g 3*IqHqԓ3|1ju}B(_%\$ x*eMƼ(%. >3. 9 E6%|qwJ0@*G<t"ueh(nbp2R.X,q4ٺ%r҆f{Ap%  r ~6}Hlwڌž5YY@zK_Y-kK.L`.Dza04pvH@Zs0[cr5 N3m9vyĖNQ`D˭Qlۉ .1T;|&1%*;B(ڂ1uMC _Mu @fvBL$.f3X=ר5vuE;M.!@ 1ElZ<+6hMv B`sDNVhk*8%УZCMl$`mSQמږu̝CCN@>y"g2,Ǿ dmhK刽<,@-' yZD?iѥL6m=MI@8lٰ,S,Au Z;Y;>Z w.u^W^@Wew1w}.:qs6hBF3HOn!64K>*Ypno}vbh[=N/ |!}3 (;n\SP3K&O:Q˕/h:Mq?|k8읫ܧrg0xexe 0]4;ڍ;z[N3հ o?v5*چ^aBE&Y~t?a=ӯ0+בV]g5_UwM; ]O/Kd_̀H@^v6 )! =&ѧ( \P?7"]Pb@m5b]{g?Ͱ:jY"FR`Gk?YB@')DOIo(X`SRlln,aWM}3zfeRʓ[[D6Y}k?NZl׾ s?쉧4ol~sxFA2$Hx8 v&vօ}r?P0$_Ĉ𸂶2A),Ɣ{$JB_dIT_COZQ: endstream endobj 563 0 obj << /Length 2269 /Filter /FlateDecode >> stream xڽY[o~ R pOA:zPBRq_ߙYr`\evwofD<| xF*.^8H {?lvj:Sq26l6&k H?˗'?]|:$ X|s-'|&޵(g;ys"xӗp:HHz5 |{ |DH~tW{? q@0Sx3)}^NgZ&NnȲѮ f^ yCϲjixoZl 1 0ᤩxJ)lTû:o`dҘKӄ=u1[x S6f ]J$8A&ǝo0dmiwpAqUG9QbjC΍pgˏ5Mg:X:jUm޽yu~3 p@AdS9KU6_⌚Pu$YmV,'U%'Wx/@Ç *gYiyGДf)HLSAMbDwI(wdQuZמE޴-«ΨܒD螠|ڙm:9(ٻ-TwZ:J|o=MH,r2_W>3`e'D݋SKe^,j01H2зk+=#_{/y{ƷWs @r"T'I2RwnB]k uvftq :)xRGʗIDQOCsHx4{!X,\$,8  FVBH8QB0L!^l C:! &G˛n4`z]]HT[ o]Or>#8%+7/9(3 lUg5ZIm,/yº*> -m:K^̧b[NTЃ/|gX´rn\W67fD5K=hPj)V6QKٸ*ٜ3s2s}#3לCfSKQH MPl yixb!89_<9 xL/PdW3 *XZ׵S7 /⻛&[hUO#^W:F1rkLEXcI~S C8"<VC'GTeTWKzn^xepwyc ] ;hrЙgy0Z[P3~Fy哌qaO3g 45!a=l\[2cl1YXy6Zl]* qy;`C >)B5{Grb:,k:2݌(u"H䨌ᝣ3$~#|ώu`#(3+y"deDL&G$#.wYWjQ}!41B7zɞ~ak'qӒ-x 3"K+9z0*:B¡VM4}\dIQqkQ0t༼6Ý4z[Xe/Fl.S),ZJ'ģ߂(oydsPdcWy16ߘj7(p9cZnYCTr^̜*6EJz=z(ufp@զEfGъ[qEr}9_Um/*,J*d605RNҰ].Hf!d2K,I5M' b$'B|4gJH3x{{`w?v6gKG8;giK zqgQ ~*!}(IvA l<Րxoy戤M 3AH͠}!:2DE"* oB $9/z";wR3\By7dh{޲7 .."a3ggXm{/= +C:M>0 c;L{cp.@@۵R*_C&>Ɂ;&r7d&}Dސg*1 0l<_GV6]gfQ!SY 7"xtOGBq ff,mk Uq!Rjx"&?v["FƧ Q; endstream endobj 576 0 obj << /Length 2738 /Filter /FlateDecode >> stream xZmܶ~BOZ#)Q/WǀA4q 튻D+m$/ _:'i 5C<̐uuxvsq7/rb՚r{:ȢI,龗2I-FBBW?< zÜE$=\:)?w(ȹSDp͝WfTݫnddO9H"'#A9v~Lhc{SIg*t] 0w?_ {^܉ƍA >Em<):\> DO mW ^6_FV3_Er{$[KK 0Jj?摏y1'Ҡ>2ij#'d[,£Vs4UYqrB/HlnQs|h'/<@7dc {! DtiLY* 8uv2_ȚyZk-t9p/rSCe\.<r껝&I7RCEiX>mbH@}c:~1jh7U7<Jֲ,\0jjf̋HD H$#NZALxܩbb?X{v1Z,*ŒE-/pQVpz>ZC}e294.}7&8櫳sBA֬/FSHh-nP#M +hh8u|nI}cHf0T mCm'٦ R"">Lq1Md 1"̊[)'N˦:)E {Ayx,umCP4${Y_C`ՠ(+iMIu摁)'}@%IS%( t:n2A?YL*@`eź~` CjNuםTCѩP`4f`m "/G%rj(%5EvI¨Fb_e1i V͒ѩƒ =nRDM'xCVa"í d&&g/+>\hSB:*bahKh׍TA6e cQNFOyR#1 k I,n=.a1R t7'v гT0=$f|!P:k7<> ݷ}9&ì ~E;ùoK_Mn4%՟ώS~gptư)8E]XnMg?}+Sv6[mڡ Kޚ=@,8S`g#3+{>6Y P_Rc6<`8Oq^n|12( hwdrOS3D&n~m\DWSㅮj 3zm]%El?LJ6$|Ʋ$o?1p w2Sh>9^~ E М&lb.+ =Kh$8Wdmm1% E<_zleȲ q|r(! g3NlGc _Ky/^o^) k{6V_=}t⨏p ;/F 5+:u@ۦ9^_]21)Y/X_U^ւ8Mrɳ''W+R&IqolaZ r endstream endobj 492 0 obj << /Type /ObjStm /N 100 /First 862 /Length 2100 /Filter /FlateDecode >> stream xYmoF_̾i h"I + g(EoGRqEb.9\>;;RR6qd yCˆx&]0Pp0 :KM$C8*6<R6vx6[̓Pl$%< Et JG#  ĸ %N&}&G&J0{W䌋qLEXq2E;ް',+\#Clh3cHPB,Pzt!ŀeEj:.XU˪@VrT蘆>@' pYW哎0Xc}US̜KGs9B*b@I;ф!/tai#V!⽇&&bdz.ؙD`Z-a"e5`t3}`fBG/j>UF#**V̪M"]{Lځ +./'ŏ捤'{i_;MXvŎ9ĚUh)8+1,8c?%4PRn_U՚7+S>^X?բmx7xY5rV5st~ͧ}_Ѝ-^3]YxC/b1՛&],.~*~ U3٤[-AxY792'Ƚzn1g|qWPzQ*~{}s۶?Ϋ7mp_,Ϡdu-pMق]!Z!e&R559lKՐ8:\sA0_aXF0wӀ5Pb-' M@V@\&g%䳙Cw!.6eVސE@8aP hP$@2 `HrIVh=XQRő^brծlC%[[߷X u^>!dzm1>J9V9[)V­OhD϶ZO7-݉T4$e$HmVmh7@iWH+Ey0PYTBh6ErM;U {ڦ*-8=YPv8f(lIj8VlR7⡞W)n]Z뻜-c*q t"~VvιO6T K*)+cz޹vx6+7 M{iv(ۚzQЏLjĠ88#MU5tX>&'ܐeC2-iD:^Kֲ K[ c>wĺC0(4iק5y~i)U vVK5{Lh(!9-] CY0^7d)oXq>%W.͆$[\2(㔭ݮ Q1 %;~n!?#]Oۏf / ãZ+8χfUǻm-CƢ.a&S7dW ![A9=q'= r %a|2b9321lL,{9mf";v抪-`~lB^:ഁK됞Q|rHF~}^_#Aѓc?eO;&=:N&MKQG \rCֆG_> Z endstream endobj 590 0 obj << /Length 1971 /Filter /FlateDecode >> stream x[[s8~ϯ};EPon;^yk:l+6 .${ ĹMdƑttt3vq﷣^H$};G`bsFQ}>.f*U),zК)pO|`sh%S'LKVHfHrHM YmMrM I+di/nM?'?o11L[k8EcϗىanJY&n8~ '\gq)v֔R5V~||T3oGÀX̷ҎTJrJ;.K#U`9[ݠ;L`O4 SեGP{ʢQk_J[b6jOo$}9FC= BA`V?O`S9̮DAQ=L0К\":p$ T'IRD4,V% \l#%R"@ W"smzA' >jѠUMGpCHpfC;l97{kIa-jF¨iRFN{ǃpdy`P cEWt!Aeȅ2t6 $F80"G&ԕAU^A^|hh¥ @d3 gS6U%Es]!@HRq JI;nS4 S|#0˧m<ô r3@RD}(@ ^#ཽ}x@K84 q 8j;r/ڶT7q> Zfy~]QJ dޗLHMˀqpOu[wlQyB+#⭉VqV=>NT,`Z6 Mt=%> stream xڭX_s6`p'͔L}Kc'v'N\[II3ʂ,M$%~gHu%X,v n>' X b(O4O%I{ǵ ~>qz0y% !:.0+FU0G/O_\LO.ǟ?9v&[~ WqrAƐه{p2jUD"!]k99сfH\Ghzzѻ_qz JvrEAfhuҌ07y17-jYqޮ8g=N/ƌ8#3;K:z1fB>4#L0W?!V]0G"urX.J U+I*G6]v,'*mۜtgo WطXKS-ZBqk4Ýjm nxUu7"b4QL f6O 5i_2UݖqEk^ J #A׆EʹYkv]1?1Y5sAƥnJ0Aы"Y=׫2*{μQG3)4h# W;\]C|A$lQ#ivk85u>>X7;/nf*{bKH@ǬU;o'miBVŀ:X:Qւ"wdHFp$MDgs4e3 ϛ¤U͑CFo qZOsf0PDO`Q9n5Pync*po"/>[hT$@-FYA}>6&4):{#@]{ 3txyWo 4+ĭ%PÎ w#|GzA+%w=oburGOl.k/6gi*?PYT?bNN٘sqش!dnak)oTD`Uf#cLN~X; @̌qCQ ~ ,,f qYƾO&Hmz&ϬnEZ5K7n/rf~l0XEn J7N?M>nn)}1s+݊P?AbpSNO$",vի-2j&X@U/*I(8׬jL(Mc@.@9w^N/NvH^]_MO=@'۲˴0V d^͟!Goo/>JjjX]62ߘI۲4S$̐=Y`V;x* $IbѦij:Ɲbآ}{%znP7㳣8w ?5% b=i8}t[CϯʅGI.J8I?vZ pRnr$nAZG?;J O2@ h% oK"$QtOO4\?E~b:kWz?X'nc uRu}Era;֦Y`̰*n53ISuoZ{gwl}1cwj!(Z!jRcηit6H7ۯVκ<9ɶ#CuNfAߒ w2UA004dݭ`vf0Ebtr6&4|ģ'WT-&c>屨! endstream endobj 600 0 obj << /Length 224 /Filter /FlateDecode >> stream xڍ1o0wm\ώc;k Fq'%. &A-RN{{#+{%sP`aVuapnwb:7еI{ P!5-%[xd(fF)>&q> stream xڽZQs6~`}'D0 a7;u'ur|m&#B6Tr HQscEb0wa# o2;{1a$Uf!|?1>:CO")SIBNˆ7.o?ގ/~5'"?)ԓj;#P\6 ?}6VqkUsFhZph-LإFPHm}bd>͎]e!ҬAֶ#BBs!d~(0xDNv62AM ya00DrP⣫ ؒdʌ,ːJBmM#.CBr(htJ8JnҐctC Kā<vu&Я:WL}! |17 N- ǐ|83zHDpؠXIRi*yVf, ERכM)Tcw-!L V]eP͸e=GUTWK`;l`2[jDǃ.\kpꯥ*->W_8q)7RKjuGewi ʜ.awe^EvȂȦ!F`lgP ST@Vgw7δ͝eqV$Qg2JH3yMYTNTgZeDN.ǐ q$텉5]oӠ.~x^dE1Vl/,ZʾD؊JȔTi+DHz(3o*Ws<)Rzydw~! ¡'O6Fi mڢ(dIDm44O8GQA!T-ű*&q@ɢK}| ]@>= (A SI߇V26+*v5[Q6]cwp\(cx"<~wO_n|. $DjfN$0.^tZԛ:UFn?zi W'qߘZDa8@Rؚ[z@pF{ gS?='t?4_m}]umVpmhv)]nO aB/8V1<CJ MPm\P`@{{ߦ. BHv hJ5U>:@ZMó=ݖ9EF1R=+"ѣmŧ#tCC6FɝTfO@y]hzځi?PqmӇfOSstͨ1A%4{[օD? ̀酠>GX؃vtPi*=OӮUJY\oWrk'u-U;(_؁k "h [Dy/pKSux>ZJkچ+>ҧ" SP%!HG9¬v ɣtڂl:a}Y a!F ?\gC.C⪤ uj(wô@;Pu~ڷ YyH} \B+yF]x^g ,l-)vt{#Hd㨍rm7W(#.yǢ endstream endobj 610 0 obj << /Length 2243 /Filter /FlateDecode >> stream xZmo_!'-pE5@^ u\%8+D+n$]wH:y}m40$r8>3|f$lշ" 2I. A$Dp]l7d u_/(Gv;ikޙLL2eD܏$*)OЩb1QFX"2%72vⰩkcME IYMNV5ksU?ҘBqgDXK.*a;[rc"Ky6w|W%>p]54K.q KތZި[*Λ~Sv!Sk; XLbv;5͐8 rXO;Ĕ?KFY\ǡ6{ߚ-9N 4ۙ&a=U ^5oN);w٪_0;ЩB703id[͝a݅_; GrV)c AZŌY֌zf:9ZJY[mUvaT-TO mX^2 [E r:T$`"&@úŝ.;w?VuTC^`['`Bkv͒C^q?:xf-#! ܂+ $'ÑєHqyw*{H+`D"xeL0D?OJxNOж`s7[o'C*Yߌ46EB3 Lv SJP&o+ѦsO9-lkpazDLQm +6ERSLVԽ~q5r41a__]Uxçl {/s6D*o+ޟMdFt<ȻA'JNDosRY> 3,$ &r}:=B>VΤxT,M#``2X"IԷPOhpT'0a>/I2~ b~ֹgrwU]x-,kAЗ/h{a<>AWMWޘtD'YExü,zLx~! '$tNWq_mX6K(#l %#A$7e;Xfy, Oyd@6$YިAY}JB2\|"ڼ,-scެ#^d1-$6 I >.h4 ^f9x}ݻx_?j3Fꣿow vx]HDdK %g'qd_Q|6E0{eCoJՒorP ƓԽz!*S#-7Z t endstream endobj 615 0 obj << /Length 2984 /Filter /FlateDecode >> stream xkoF/K2wRm.PE@S+D|ɿRĖ? p9ܝJngbًUL>b&g HDlw1[T,1?ðP^Ȟa@M1jeǁ}{ gޠњ'gSv*m7ebB{pZAmao̪)N":wv헕Ea䂝$`{ C#PP#(n)־Tlb`Bdif 1  /-24~Hb@E83t}Iw{gl02Ww(fsjkl ɤM[3+VH"a^o)1uϘGCA?@h0 $BYCT!ƒB^C\efkȁǸ)|Tj/IkΤ`1(GI 8~msH&?~Ex#8P0凸2iq X{rzSG۲}_V']j{nNZ.}Qէ qhUPaHW`4t?|Tˀ)t.ݶDYLBOT)e?EH鿁[*U8%tܙ~_$p/8q|پ=mӧ?=_ㆂɴqקY7Dƌ ru QW>*lrjN-xmd%lᤪF-d꬛e?G.h>p 8BPOCh4D(ԟlEEkЋa0l:s5>,*yմ_" pcYbE$Hmҩ4䅩/lq = -r1R-&⑵r0ސb.\3#'ObK8>^UF!u qHl /嚞h܄&ZP>v\!U`z+.U= 8-xڥ*mJ程7Ls׋2+lJaa[~H%2VfIi.{7u(VSr/IķHlB Bʖ&^/R=?o֜sl9U>vxB4.F~&M;[[?8Ę]I̯~~}A_oLmvi6&m.XMiMXu+EV|n,xsoU Sx lwk(]'./0S3ȣ+](UvNY4U^^+b}Ӓwy5'qzS^aoE3u@t]tP|gCvk ϟ\İ9* ,+mC$d0Z٤6=B*sgSDH)1.kZtA%ppu$qQp(ro/U( ^ 4iՐ0vppj!n zaVCAO~C]FhA.yHЂC ȓ֨"SgXFaisUV C8y|ow[FhS;<_ŦA=Ԏo#-5 OZ;}숸5o np+8 BBDP47Jcs@NQ=fk(1T _w2zPA)tuD0 W v>$Nߖ]LIIoyfFm1!h>:XU7K(~$}UbW Mu)"/ ۥb# 0’]Xh Utpi7T@CFNH8v: Sܚ4=M+a2NS8K2酣SV:.wq=8)ő3U^AU nL&|Hth]4<̲ @;`GM߉seWp mHY#qpW`Άru1Zkk^eVK.~ҮضMW9Dh,=ugdI!;z郃38żdzQ~i'KmLGW, Hqyy}da^]O86> stream xZm۶~B/f,^n:IZg.e!xxNbK _ֿ.HI,}vO&1Ln'~XMn۪ɗә #/żYN_صLR2>^z!` >Dg!'ϿƿphZO g6wq\NqĿ0BёA̸GY~XF?fPLatv\=?_[#ND o}z 9aW_Lp` 0GJj\"R %z+%*O& _kxqYN>5)#9|oxM\)oųhOe f'yRΔrU\1X+\CCJJw$4h H}C@Pwp94v"+Ai`E H@)3M7eLJ!䉹-jTh(ϱW*j׫|ᆊnZűW,C֫545j1R\l0rpm9vJ}?Ng DI*u[}zGQc+ر7#\|0C>NN0@LjC@sQ}+vS)~(UޭZW9JawGaX`ة a=pC6P4<$YcP211 FP3α.EݵөC.](b܏NrFy6|'%ӑpٙ2Cfn)Yo2ro6xrDYR$kY__xwTu>YF;3O`֚)#z2, ID. M`sEt5͵ ډ dmjaEcA#y$UPLTxiM=< |YS'ow-uzU1v `Ǣ݈m)9(|d}5$sO}nI4@; b =#sm#KgZ}OO,K_í P+02#)Xw^VY]/cәC崙U81 mNlfRm]l[ksZ mM@ǔYz` SiY7IFu2_#䬐;+@Sƣ~R xq\HD]fD?^fT 8m^9.^CVɶaOW4TMFMetLܑ5(r }WA-h;|KrWzY%orD* }O"~o)LQt)%BmT45u6`sIf, fwݷET퇻*xIZ9zаMMk3@{fǚJ@w(a4 ba : mdUEB "GMޠ݇3wjNJ{E)=VɃ=SSDQ~yY\fQ?G v"؁~:#EY7aL4Z?5e.W~_9V4-*Z{SMxl!:! [z(?k"oVu:6+;]x( 7i%u4$g ¥ژ^2q}5o,ЀUf0+Dkꢢ).W/Hni+. dj<{ȥ4x&w%'wԨh+h2S>'tMO#l=a)NWbah4K+75R]RW.-2wٖ͛Vv}ssY%*g{y㶱 SѺFUVrԻT1@B u)f^"~]%{fULF"]}jv ;2d箖t9 x!w,!],ۘ ]ۏWgTB!C8OqGgN ++KuRqQt ]r YY mI{:X3xBnmк 3yqɮ}H~v1K)y{I_@/%}8]%~g{BS8>¢lu&?3 4Z#HOsy.^@m"9*h3/ 3$@WB1$$tIhWс&4XuQ ꮄֺ|E #-Jje^MΓڸw.q4Yҵ*n }pn4} ^wrj@Uo]~= }WKVZCΕ 'yE }2V|XȡmV$./ٻwYmЉAYLI!j,0z/be?$(&w#-|< C:)[2D?>|yzJ$QjjחN=}?OPV(@<0SZap*{:{T7щ?w xv>f)܅'LjE~WJwWir{gX5И*d z |әcoWRz? w1^ ueP[ngu[tn }޷lD endstream endobj 631 0 obj << /Length 2550 /Filter /FlateDecode >> stream x[~/jo| `'@ '(iuZ$e:wfws[Ԓ;;;PԻٳ˳"b<.מ22"2{_Vj6aW,wNv #!.8{~y:c^$% i-Qo?x8>2O|n7g?QcIH  Wݮ#R3J"3\N_{;WM:dOԛs 5J~v^꤬~YO 2 n®rDe\/,(QfL+fƔ괪ӥ{X;֖ujMkD;zN*]ZJL)߱G%"ڰ"x$n4ڐ.ۧ~b?]U]%+.n++R`+{ =oB+(/ewO+nVq? <{qpdAL(c )x@q 2JImkJemw0G,4=+۞Nr0O 0 F gIF6'sP.M&>%0O&̞pRԷ[=miO>Z#53ͩFOs J"HEQVt ~VE$bDQ_L>ho 3*EP2T׭k sjIiD1"B#c86Bˣ$F NbXn% l(*?FƲC${Q 1M^qiI\eUQkQGE.[t:7PYگ=a+pgg\J.յ{Z7t3X|[~R]}X@ђMe4nG¯TyT9 0Ǖ-0DH- Ffsɤ?\5>b`Vn "D ^P{Oz*pCZ_&UD8m.#՞%1bg8d Y5[av+PJ2wuk b(?$/Xd"/VUo ^~HSNr#릒e8kKMbݳ-{XR+ئ550Fa6<ͮ5lv;[l jsmYj,w3YeiGn3H.:(!u<1ɣK߰5qJ6RmQNT4f8Ѵˢ'/mb/iKWVeHd-O1糤P$wOw,?KХ'%ȆjJ0$Y({ uRC2*eqө ɪ.yGM~TW&Y])Dz!Hgg˟1 ɁHːa)h*. Ͳ 6Ωl{p LMRo7Rfh8$Ix}Htm01W "tm׺`T# $h,YIV@@J .An a$l),q<) EMQq/'1F!DG TRLrȇ% ۃ_̒.3}4 %zˋgR 7m$+'@#YXpJ7i*W44>i.Xl#hu 07L(EǸ cs|(ǎ8 >eÂTď/U^uz/W'ea ˓ci=g R#7lb)qۍX fѴۖt H A/k m): @.6HO*&ްDҘS|0ܮ\[l偅DDD(j {K,J ͂ڋmgl`.C5l8T* \3"N| D '`[(RYd:_m.ſ:^a?^|@<3~zee(;Ww%"< yf+0!2zP+աRU0| A  ` D233{ ˮNIw'h-*^ OT#A9?EN<0ốSM4)F #xZ?> g`2:_M]ds5e"~5:3~9gKɶ-(h7<-D%8asླIv> stream xˎ6_!d1MR+HshM($Zk#pHYiVvԘ΋3;K;/~y'8n@57wTo'] 3w X$u](]o=ށH'\m$D ie0S;u7$.hFYvC7eo#EsY$ xoǯfE%UkKq}V~4);:K̄0?` XyFofTº5xq,ɇ8ۭ|Ņ7M^L%M$3m83h[?~Ix,c/Kg.s7FΛ䗶H-T>3  VդT DO0T Hs*&z/BN?,&WU##\\ QuL r#E@.^5|N I^iSVidpD8Yίi&/TbK].w 5jAۺх1:1#<߳hsk#a+h:>jpP`'d˭B8Y;kG \:U\C x,=2-k2a AJO708?"فfLm"Ȥ18d}4`0Nn~3JXhT@P^>zGBGw@CQPLD`k4>LM] 5&Ʌtzz6>d{twms}׷/RbA)ð@`9 !|* كz{(_Z!V6RIS; 0xAuaZI;!up)cLאy3 xĜHI wm RN&is!h]mD f.vm^l Rv +H$5X'f\&c(2 btk( .Y/l&7ė^siS]%?7z̄ǿWxԼcpGo^Sp\a3,pW g_5;Sa;c4^OuĖ*` \}EcatoV֡ɓC.&sy0y ({Jz? g$O1vEJI&ˇTS|.?u A8tK9X7Xl^D[yF`X\M@d/u endstream endobj 645 0 obj << /Length 2390 /Filter /FlateDecode >> stream x\o6_!`CS>mݼE r,Ԗ\}lR;| XԘgF3#:/Ϧ(I0\" f<2"2"mrY5blҼN CN*F4_zٟg ơ a):o~ P"(6 #g>]hx1J(VtR [s 6|}-F)6*-]Ǜl,uiR ZOfa*7NgfSESfV]VU:/31Z)_0Mbm(̋vJl|Krl!=gjH >t%uI+l6Ji0,H3%ds%he[{B'2sк=#dDFl7DuD=EDHO;|( u}&ye s!%'J`Fb};+Ӛ9´lnSh6 Gvǒ1wl3v05Yg7{816l"lspRz™3 7VtVh>g\9xJCrS!X"BJi[ҧHėќf:Hi)0(FERlfi;Q;;9VEXa..0L@xԍN"`9c'6=^a-ҽ -Y2Y}녭k.}X;YCTeH6g&y;OG&ލ[R0HƸ CaWkȮwͫu ?` ØP!S$oCZXGAHt\R]V>߹!3D*+a%L1.+ _$&`jM0 ߣsE^ O~'EHm`a6yVsym bIWz`~M`SJs;/'U j:ioTV2[aeG:^2:s6V# %`6e9.%xK g)U@ݜg{(} "R@lcx!@g-j)RwBQՓ~X- =Xp"]CQ4bH[d/ f8IظJsle: mʸI fs\}0وt`ȍ0NwKw,B0dAxEJ *,`+0Llc'a<;IbtNMUlgk)P[W /(xSXj:p`M*F(G>aJtO*G-޻7n3Z[uvo .zaA+Rpݔ/yJa 7!(dD^#F3` QlH$}\oGsan[<@  XK?hp?bC!av:L*upkCY&2 ]#ZI bT={6a}V/ov9`xrW[V Rtw#ap&4y`=ə8A #?0(C&K(_P> stream x[mo8_a,p}[3|Tئ$awȶ 'K^IN7䐲(mcM[!9$g3ϐ ]˓D8H].G2$2eͯ'SW|N:"$K*FB']|CC4'FG ^o(&X'oLԷF~TIU*׫țns?r.('sSxfi}ꩢqJdYmeXʋx檶U͚ϑx0`όALSH̃;BaŐ"% kHSVH긬Sh\&&+'\Xh&W̋|Χ=ǙUu0Ųԉ!o+ӲG=OЬe6LmVu&h#x$Qo>Dj(| Ӟ늂1'횠 L>S_ДP-gS`[`_n]Ql)][lH8X-6L]W4rNjEQ=961>ױl`*oZ܍ VŶB pfƽq20Nv^NΆ(#DA=6% Uap):;W9u)$>^{0zڶSk8k͘=~~heU^pž,R⣎9 CB#eb@(GLУ2-!2gc_B9X? ).j> A,Z"IDP /B=_ 1QUɎ)HvGy4W3 4?#g[j4~eT~L=xmև ; cLk&:na v7$dm@q0h@5p N&,pXP] /o-'b[<"T{4?`n !j*ǚ}mY~n5 4x0B"pJ $XB0ѪiYaBD( rjri (.htqI_Zҗ!<\!# L!Kdde(mY&yM?ہ0[Xv Ͷnimxjpt1di]g,u3K?:nt 9FqVfO~5s*yH8cU{XC݄g}Da3&>>!S}0,xPZ3lOM1AtOjk Tu"}8w3lPjS'8\̱R..J/KCIO.RުZ)&iz :+f]=͔`1Ͼ/U ϒ6Iܨe;LvI/dIiaj$fn)uQBZvLdHj9̭ڦM"iIr0Ç%$Ir V3놀c4)*Woť鶴9Ъ6~E[N[=u *a2]$,*'~>w47g_y:v|l3̾'mrsNB!':'|k[:Pl@s9uvlXa-|Q-7h|0PT-?  `e&x+FP_3}r9=?ӋՏ璘^_L^ CT.4 TD$PVj=\Uj&)0-B9<[Gb`9H׀D_W]t]|$]lj@_:MrK AfZ,Q<|cBMhcOyv8\>SxT9d6 'w;swi&y\-?C1 |LUrjp/J r@f4@\z>]~J竎ٔE]̋fGGH*{rpVB=ut͵HW?D<;{' Z_{q ~ +AU7pS<\ΐPXx `Y{EJI'-UxINsloph򅻏|\W:@4lIgf endstream endobj 658 0 obj << /Length 2059 /Filter /FlateDecode >> stream xݚY6Wy17M6$3 &HCmm!:I}dvay;Z4EWppo7ϾgQXR)B#cWw)UQ\r]4Iŗ.o3 H!<}͟7 ɑ*X7`(YQF3ɔ"\&I0er&h_XYZ4h{IzU0 5NL%lq t`WdIc]j]צVkݤJhܲ7,0vrWz=c$+Z04;SBP,݄Ш&pƥn}E/To\b[Ӯ=c-B­\bYVڤkc^MUf"4|퍹L<0lvŪҫz(q0j>%]h|JZ*^^w/fsϏ'-6 kv_v:<uKX ^nR;O;rM6uMp@Ft⨯>TӨQiNٟ %mAte j_=:cIs Eq)Nb`Wxσ.VCE*gWUGHq^\S{ &rL02Sg[FRg=L%qQf_ ! )# ><~: 2@#! ] 12<jS6>;HrG鏈*hZAzhMV[ޞ^ʼnR۹d &ϵ@ 8x*J5{UR]?ʶ'`~sc>nc7Qp{_-Xc$x(aTФ)/F]hʳFP%Ű"D,}UAU0F>}""Ad kyKиC'9g zmũ7&LNLNo35{6ەML!@_q;%{IvWVi3f{,ie20iN^̯okGb"u=ݨvf *L,U#/B58lT .9$ʏxzl\QD=&SIiV7MtCNbVi%] 7PhY endstream endobj 665 0 obj << /Length 2432 /Filter /FlateDecode >> stream xZے۸}`K* /[<؉TؓH2/Z^<|}EjƻIɌ`4ăM^x*Jeu Q)SY\wU7ԛťLrL}HƔ& 0Nj\]z! RX“`Y]+peipc[USxۋ^pgdL~;7 2G (fik.5UQfiaKķu:)j3Qege._YVTпv-PB:Rг3ha5tL5C]v׮Y楗.?ɦlz_}3W8%8vZPʬq #2D[2^jFCbFq(#M`Wxxآ^uRْR0d_Xi!u3}TcYnzӉ3teثа"MOfİb-ߤ3 0DDs87-4S{V}_\{:w`wC@Uɴnp+ aYie8i,%b۩Y.Jݑ^G\dU֬Vj[*/*lZ]a5$uLG "FScq_|&8q@ڑ|8D2 ;V'&lXYZGFg^(g!G1O(Zh' \~x ҧs(cO&Neq.%:EC-ccThJcYp}2"63ưg$m*TCSegwbP 2͔GdhsZ\<^@7-by leQ@p0V{&ۙ ē7ZN`%!Eqx M$ O/`Yv_ѱa+TNWCS5 KeiڢV@i胙š7p~zqyNpF;C(B< пus"Te׸;,ַSm."^VBS +o##(\uc+ᄯAPs`hWPΤ;܀݄F\4#zpKӋSMMY6{ WҌP<|9-8TyuQS^ﲩF }Q{5M^mVT'RT'J }$>2=A<ĀzBtgxGӝSt8 DwЌ$31a ]Q@>yn@~@ұ1lf fga )6rFHv\eצ:f~Qk;"wQ9}Q ǐ";{udMh肭z\m=T4g]W/_Sa=]~LRM`/ɠMt?!w72Ͼ׹ozߊz0KEɞYg8]yez;/vxbG)E,uoJZA|U4 N"ݻtĠŒ;ZV):z|X3J@a# -kz bvmE9os`0n̾{EUDp {쬅8ǹ=vV |O q`~GS}|Ώ:x~1q٣Y):H:E`cWyX[Q}(tD{kTT1$_; @AƦ'(R (gW$'#_}-ڍazh Rm^_ݡ~3GnCKpE96zc1(=-|EHNLp9LL%NZTS/᠞֬\-͛\H<=m)t|fК8Ѡu8VN;G3?UKiϕF&mVqQ/vzy7s-{kv+1u#NuI0VS5'Z|/0ad Q,޸!!Wr(j窤W?CA忥 endstream endobj 676 0 obj << /Length 2351 /Filter /FlateDecode >> stream xk۸ >Cm5ivl ٖ׺Ȓ#yw,ɏ$k;= Gpf8/2ԻWgGB{BzW3O!Q{*$w5^U~;҃_ju\E~naϒ,`D:|}WgoС ʛ,^Og,1R=i^g6IkL&d|iOS'q)d}iYCEA@ISӔ4׿=}:zǦ[1KkVﰰ@׾YLTdOVze쪔id #įIޤM5侰f/vV5ˮ8uy## 6ň7AU90n~7888<͊q1HdYSOAA7s?@/"p&(=pqF#Fw#C{Z,4K&Ey{@EG:A$n#Rr,z?M<<+Y&7xNŝL|5Y>IHVLl^TwD%'nu -4WkõЂ$y-dm8N\δJS*3I9DާY#evkjzG;E^};{+DY1qF.\^>vnajfn;8`߄by.nBIH6M X/Cd~3`[޽bl) "kEqGr*U9P^OlXw;6Isq۶x +emJr'R26C?&=dI,ߓI]A:>=tv;j)oB̭J*ז;/мi 64Ǚ37`,Y.^x0Hok{v<g1[edVmǢbUuу*޾&TgIl;َ[Cnb2÷l>w 8a Ţw.|>yՂh$4!Cl^q%dinddy4MGgMLޥŪjqZ,VM˜wZ4-_z.ڶENK5ZR{Ĩ"s_n[\[Nߑ+*nQi7;BgnrLdmG͛kaiqlI8%dnIW. kEFVA܄ZķŪ-y&­c9;&n݊Bӆ`[w{ĸӎ98-|P[~[ "[ٔ O!J> (K# >k~?|PK7.]tm8)FB ,)#1Cci7WFOS'gо2ePD]V.VC@2`>,luIH& \'3 /zy/* endstream endobj 684 0 obj << /Length 2556 /Filter /FlateDecode >> stream xZo۸=Vk")nuإ9 C(ڒ+Mӿ~|d)5}mo0`QQ,gG'R*(Q"4iOףz]<ѣMedFBo:!hP MM8ѕkp]D~>^=U gAA t#P)ŽnE4ё$*]˜7ZAYOHNRH!Ԝ$*J Th'ToYK+Iq^ نt% vWUy]䋼˽%C>U&:h'n1S:nju4vyY~D+JGRIk~W_7<ǫ$?O|غa kɻS.;+sY~TKz'YUS,M,7sدLe"!,Nz% f2~j*2mpsLJJX;3n-ī3-4f &w݌.To;pGh ʉ@*P+ShY*n6w3ӑAlk;<Gco3d# S 0cإ.vean _~58QgxҭBwH7FF-cb/jtr ta)H̀٢۷G Kݹؖ}Sa$.W?m@Yy8lkO,S’FΔqGRZ<1Gb_lkrFw;#Q`S{QZE"(-1Qa+(BT /`&Ym BgTIÓ@ZdsWNIYL{IG~@jUc6yl#FhH-7Ѥ [hj]b*=93 ~н~̋-:HE?t(΁t~0Ns Nr3g'7`naĥ?z.N>X-,>{<"`vda0p~~-w<\#2o+m8Ρl>1-T2dûDz˘mBi!0"nCz^b,E9f1ʳ[-zx\#DS!Mow?|*N:0Ȍ}nX@F+Kb2"P(01^E|f kKlQ^)@C57)c)WwxЂC"<_٪'p_)TAQ7tc_B`g=(|asl endstream endobj 690 0 obj << /Length 2151 /Filter /FlateDecode >> stream x[mo۶_pmhLi,y~"eY5f[":΋N Q(p3 |B 2g[6 x5•&GDM7Ei~13q<+8K ]'g5_i@J4-W++FN [`Њٍk:CCR%YY7 AZiaFe4+8T֤ȮaɖiI;U Xc 4ݢ p{QĚxU@:! }};D؀ ?wX Nl?½FH?VA&ps(F3\!x~]]rC#ҁs"٫Y! }[~.pa[D_6Tn(˕+P`GhU'{]_zY\2ɀ ޅ" o ijy52\f'7&\d~)[ 2$'aCXh{S:X#ES[me4i34Wu)$mS#&zMLu'\=2,<_f7A1fc=f^0 b aHeL3a'>7ퟵ@?͖=|-G Z ⯹l  ={~}; 7 GVTxy#A1ڧCLѢ\&=̒QDYƿ78Q !Յ9b2Rȅ2D~1]ayxclT-NwCė gt/:V/WnD pSiYcDd=m@%Ug1fḽl/f{b_Gշ~d_Wj~Ght5ꉢ$t繊bSl%m^! 8Cr3v]J1~ ʡnʸkv0_@Om63~foL{?VX9jJRFc\ޜ34J(u Ոa?Į8LYzSrwX]rW) kRJbqt?D.ЮqY]˘IRȒui\h*Ԫ>p!ʲ٦Q]T;z.|OVfgZH+LƷ實 {F=DGd.65zN)]T69(teJ endstream endobj 696 0 obj << /Length 2316 /Filter /FlateDecode >> stream x[mo8_-Ѓ iԻv[,v{ao(6%$lKK!@Dp<3#*&gX(Ty i 1 &ZWbZNȿ}*SI AN8#0%"e0];fS`mZqa=fҲ7/R>XMʀCw3$|?V+{Z_+UU*{N̶*lkUfHL(tѿ%ʍ7xiC*U|<ٱZ5@ )loSp0"Ō=E۫$"o&SͰ#x!&F2F7?N&mZ%fPnv_FU`@YVaQd(QQ$T6Fn]aC@/!7 {ΝR D"tJ)$EtJ7ln_Nƾ:})@0y8e4 C'J,,Z#F=Ved02K9.f&~IE^M K~y,͓zyg'g $­Dzʈ:⎶d,49@XFs6=e:ÓŸLjsbi*@#1,n_}HcipSt`Vc5,yoSTYohڏM5OCj\Uލ1"f(ޢ#.D0'ّX3ۇ!_=vpMh邦6 PD> :Gl(!E>$ A\L܉%o1mSh֎8pA02FF %N)8@#?NZqbUi0VedMifs.͵#gs}r(wY&WŹZ.izrh8R$kTUs }2pf"w?5 ަR C!ͧ@ ahsIK͝?dі2$!ԭE,*UeZRehSZƬuD兣TeVZpP8Yf&ЍvTjB*7v`8vKe 9gۡ/A$bKD NeD1 {H% cI"oPBi@P Ǿa@D5 ~R~;5/Lj.?Z#^zA~"%,|RJgDeOCJQhyd0ޙcF ~f To; Xyt;ϏB& %V: vyF]pT]`DC \G[>aX4E)90F!x5|T>3?$MXfJ׵ r/ӿТ)5e kA}ze3s%6~2^0ZNS~FILi֔)x. TBxPa, س1MOF*f4:dA6CZmmoU鲾!nW#ݘm򛤬M]\k MߒNE_GW6w,`/`2#FH7~3~f|s7ʁC4w'$͌G{ٻg7:P=q4"A8"vP85΢ėx .Cw}Q'u[~ܾ£[;ff^>kt5e5fz0ɫa.rB@ֲT7iCy.M'Uw)sj*N iK*̋봾>oKo6kk[ɦlĭ (ٞO‡C'; "feUuP&aOId;feGi>/[*e$~@*W9 RZ^c:;Եm&< 3ؑ iNcP)0$ V@BFĐ]!1 ZIR:!N,+S¿ZNյDrfT9}U7ٕ{ity ќ4BL${MݵA+T8GpQKj]:E~ajUt\ endstream endobj 585 0 obj << /Type /ObjStm /N 100 /First 885 /Length 1728 /Filter /FlateDecode >> stream xڽYmo7BH|TE+$una{NތNj^FP"%R"A-dg %h~5Xw |mIBS jhBfi470q +z*ᐅ|HC֬ލ*CBXC?bd? `M34R Ha]֕ᅨybF!Qi$UTUڼ{ NYVprb˹8 z(1aސ54 ͼK쌡ye7h#+j %QRo hĤ%`HzU78i bг9Â7S ZWH)i>6mAc7vXO *>I~Xr|VH daL8{C ,=0>T\GF(JШjG|IWVVՇ9PVa#oLkI)٭ q i! EV7{B^yr=ݓvBXnx)a/0?uչo_0uRaҿyz^o>52+(e=]_//W},~9j.pB%9ķ-vV/(x=ݪjwj}L/vv_R\@\,2'R,RA=xvmwn=~x>y{y}/x{b-EJ/Q2N%ٿlwsLy0(j``jDGČ0n_~ .@yq6(<)@tq-/0B  Sf?ˡ~̑R9ޓ9}V4Z8b7Z布i8XuL} }NqIL8 SG̼TYs@繝ĄpHL U0 Ҡ#^N:)K2/-\,g>A蓃yn(}ےb(jqg.\L;GHso9$Oa^q IdıByt^(V& bDe3IV$-YN&jܖb)1ъZƆ^#JnT-֬SiyYMag@wd=qigtfԃX4d-6I)E=18r/u;LS1NIH6Iƚ_#Ly_'2ӭθKYF~й_>%n~O6%U" Ȝ/;y[s"ts)\-mf6> stream x\mo6_Ȑ 5w][ u? h;CqD--;Jt6iNadƺ;M[7, 4Ғʠu0)"4Pzwu@Y)Ng\#S\Ɉ7Ays~47 G&Fc0W iPR&tR LAX"ᅋBّJ!hSrM͐ :P#-dWIke"Ycfx2@ ĈYA:z$è1Yw&9tА0-yQogK,"i HH]?0u00Y`%$$#~nz;`a5.B00KX  INi+̽A%:^Ey'<W91~I 1^ӴYƅ%RQ2Eؾ)}F め�XdFg&c[7QR^O S޷lsh5n}}&H?^fnKf*O^}*9vU|\Pyw ͣcHR=Y`hiGؗGKG3! Դr), ҂ԥ>XAxdK_ k}[ Go@iҲ)` #8m({c-4,UXCI·,9/k-"U%[9q*\W_BݯGw8dzuTj7+C[nv'6y4cZyu@ !OlW GT.?$6h1i4$S Y`b?9$ endstream endobj 706 0 obj << /Length 2562 /Filter /FlateDecode >> stream xi۸ 5@P9")X&HEщE]Djtx%9=>JM2c~X `Qnq'ZVox?g`)Yi ?[dP/>|B@ ,p礓UzV K._[_6m\2(Gj~$ Re\I-jq@\l[ѬLSjӨ@vvlnimk)0~턘Gx:# O7~ܶuY wql-Uv!O+H6By%Bde f5D룞A)f2R_15 &{Ԣq!seeS#9ֵ!_0˔/EzL!LqY._uOG ծeruXwwtRS<vjqot!kh*9q.Nv jؕ`3mLU݂uѾ f?ۢٽ Xª;BB}'@R]f% *k@e}uSFӳ9" fq"ϸWyn&E`Hda(ޝ;~>N^y瘐!T} mEV@g= wGBw`"wBŤX_w"ZOI9aM!nD\󻣁wEK`p B xo8V:ຩv>d!jm7]ȺVz|7juh%P1= Mp6f[}bie:QjS PdIK+^g5 nڻ)iǀLшưg .)&Oy}a&jC|n_pgF im%[¨di{ ^>Kte.fZiW8 Si:Ug%u;nAI>I_lMĞމI3XYw:mv#n2AJfG#p޷GH>| Ÿs%g|nAج74Ki93P} pMmUƴWfgr*;\=ƞ mE 5;\dH+L۠fQ'%P WMi!*}pMzqw`ߗ|; w!"'c+vw;I$F endstream endobj 717 0 obj << /Length 1977 /Filter /FlateDecode >> stream xZmo۶_/f/(ۀ Z MuC,ѱVYt,~_D R~١aPkT~O\k 4K:OlmW6G*4Fg*?8y%@7u|vmh #ObXWZx$C:K>puZaT;8Xw{چwH^5K-O<Q]jMcª27Mj?twuAO6Xlf籄.L5,eQrg95t;*2dgoԜ}Udeх&1]Ŧxa4l'ԏfkHnOq|,ج=^mҍ!zX eӦu3ݳNk|ն@Dop(+L?2xh9s&gI^71u~;f}0=^ Glt6yJ7E;+j0vUk`@Ɩ2&Cey?Zݖb94sY@m}D/lf 6R(!pBn$Qq ` j`pEr(Xt*S?A * "Bm,vJĠ<?i~ʼʢr+8)*=<0Nc>=zH'q5qaMVKTKC(φt 8ӝor}Ax. >,0rٴ bD$[`(%zjxA"%kAe}0~΀_(9~%pc>>g''-iQ#k&2907oc3cc:{ƣe,1E"&+aίARr8þA)q7R$Dk(ANc^D=.r+I›e"/P3&GZߐv{>P҅YVY]ڵfE`Ģܝ_Mg}QODgg c@.,S8?b))iFmOOel cf> stream x[mo6_S 4_@5Ŋ}S[$Zm)EZKt e1⑼{QiGӁAFRk f罣|GTr%EXiҵF(#wCBx8 N$G `| 0bFSK yii~Zܗ- LBm;W 9XYU@l.4[&% 4hRrIÐ &P0iu}_FyQ*>w")b3'L% ۉÇdNVdȍq0+ѯ _L$ŋGpt_[-f{=bcg.K{Tu0"|D`D 2Doaj%Leqvx'E`c5C*aC[8yF&QF`iG*4$@5#m UR#MMjX8rU:u?!0IX 2<$Pݒ!іQ[P 08ô7P^ {O8BɐE4k؆'A4$$%MCEӒG>s.+?ND#-M~)u۾v"VL PwzMHUH6ϳtNy\\w ]2 c םU c5 q0qX=Z ۤM?~n$L>'H; Q&"흼8oEDƣvΗQX!etO[U锍Plɢ5U7lxI0E>HY&G?d29 lW+:{Rj%\/v8ʶ^0=f>WG;'Q:qPH3/&NNR> stream x[n8}W}rz;`w؇MOIm&vkK$'Д/r"8 Dpp4GP9 , \%ZaiAFGQ}3q1I7M]p=?wwC #Ƌ`!Y %#l?%DdqJ>wRG-"#NvGPƈ $fiD$Ȑ̑K󾜕*+%bN嬴fĢ5ִ&J,ң;녝/:o}aq)Ǜз҂}q^|c^q4tIqFh[XLl |ij멯)M/q4ܰppąpuA"\?fq2Jgt= 4ڰ4N%,zЙ.q$vRB_+BՏVT#~Gxw(nƋ_r϶' o㻃do˄;\w[+ik%m"і_>7"̚7d%L?o?> stream xZmo6_(fe:tm:lhB[,zqE*ts"@D!}ؙ;yr1  {BΜ7Ӳc<2*ɳtKʨA>㷧ώ}8";D}'^y?s0ba|lF-OLWGac4{f`"0gF{.lLx%,ZOg㨪d:xTWR>y,Ȇ&B @`\@粎O4֯u Wo"5ͥ wBF?=GIMs : X) IkSYǜJcgrL"ZRybf/N֞D0!c*P~g(y)sX)'@ɱ'=/ x |FpϠwY]2 XIQVY/\j+gge3M;o[y'ޫ#^M`F!gJY.uTیjH -lH28ރBHUC(-YN98+HqE߼HIpX1xU:wG唉84?!>p8(O2BψvuAYVya$ADǔ$)(,6͵*OE䙩`@GZR(A@G! D $`5YA4 D;8.CcE=}UO;٠r"~JISnqPsF.^K]\F) v\1!xZQ֐2Vsc'2cҔ'J*.MCs܇e3XzzwdrQUd21NB"eYcD&Bk,_&b?{D`$Ωn[v\!m珌 У.UTfBw-JVBO $YYE`q^T&iUD7^Ӕ~AOvxT82b7c阌*i;9ItY2YT$9r-[io,5ꍭ_gӻMѠ_N E5i^5! yq|?Qh B=@[|'WI*ߕXlpuZ}bYD/Gyw)GqDOe >p;:mudM&0<|dj 8ǂJNY2 %\XڣNzX0vIҦxUBE 4v ˪$6KڲJeGtLG:j{EdRt X endstream endobj 743 0 obj << /Length 2095 /Filter /FlateDecode >> stream xko6{~pIzwE wAr,zd7C-5Ӥ"EQhapt۫\Œ(E"YGL$ d8e*qrSdlY٨&_ܻT$?|ϛ_#e^V0}Kc6E"z+hݳ6wL3vpvfYH0A v3KRD,A *ր@%~bޑ3y (ঈ 7W\XF~wo{)7 bBdK ahs˾~2HXSEVX>OJ}u3_ENmv1/#!#Nzi^̬bI}Cavl6r&w=F^.bDY2wpw`O`~M웃j|fhjaF/gfkg1PSe} 8E"RL! ys!0\`#ʠx3'j*38Ҏ:oe %mx2Ic}vgr(O7SzC"oۙ&J ڤ)c,PNs-v`ߎ6 %BuGe-3A3{&SpmkѮQ i $] >mwj4|s Pq9ѨK8ƈ$ _drYapt/ツ_=>.|!Ԋʀrbk1#`)IuiV坪P3ko~k=sy>ȁ6ɷSDh0ug[h`$p=+UjyHS&ÓC ]/ QvϚ늚FlfxQ~FwMS9{=n~sZ 5|W⯷}޴`v=/v&bzs7s;i?6q{^I HHP@/|!R ;AW6׀R] B"OG\6(L;G{0F5vMP]3^0k[9]96zYU;<Wot[ 6m!l6#Ξ{4M$I4L֕%,5]ۈ1v`c>$uZoէ3 ag:GHr)ҙ tvgܽNCNXQ|TEu3E. /MJsPIF~^cHٟ^$Ԉ=a4ሰ*nmTc=]O |v,uϥ6 S)6! J_G%(`Ҕi (gy.W|V Wy$$ }3lU%:?'sz"ҟ`{[wosu␻t$Sׯ\? endstream endobj 747 0 obj << /Length 2060 /Filter /FlateDecode >> stream xZ[o~`Hz/6nuSE(jmHf/HZ#mݙٙon$s`ۣ,t"wWN@ 1gt^Oʯ3oZ\2-3g<\ K coa` ?S!K\˧WE&N3}6xڌ]>BȩHw.! m;̂k0eK0E s|F&r`5I> !4>((@TI 1at]#'Œg8%uSvFaI5g;"#T~4\Oa 2 /:.RP#? = =ԭ~|Ҍ DϻPK3R ".va ?ē4)&4֢`jn°̮.R6β]ٗ0h[TrV-3P!(78P|"Ug3s=nʯy i[R Ҁ+.ǔv5C|b)tUIz *E]6Gn y^N]ᄼ.UYP)l`=صrF "(ݨYȚȢ }@jxC]eAFu}_Luۢta'Mua[)هT]KĒoEݲ֭ 8EiZy!s-38qXne!NBh';l_xL뮵]_YV&.e=ׄ>#xMt _]Cm?L(K"<6#JC( Jw2]&"D&j l F " vG(Ҵi>9XLVINPɳ|~R.ؒ;vya(jFRU]OfSjyT7nIM=4 }Y<~]57À|zU||> stream x\qo86%.6n;[ӚvIv6Exmt2p3$vY748~矉v˽SX5òDcL#A?Wm샓ȟPxb->`Đ~j}àGװƤH1i޻6W ־ghe1z{oF=.TdУK{-kb3Cj&3l 4 -GPz_ 88`# Wj;-S1eje-]?xll __G%Ӕ>_50Y5@D" WfIuӒ>ZYKBd-d;%QLڹ/7nԢ}_KDu)ady) e)m/E*VG"}{Y,)f {@ѩt`t*l.5KLDm 'i&1ծ HBYY =ʣQ;~HQe"Jg%0P3 (wL(qK(ldZ >c$eP@ضXp3L ?茎ݓA):DH;@;4Q06r{LI)~e,UK\g]Ӄ[=T$bG,RUHc1f7p2o4m51OΒ!2Ç+5\3'n.@0&1A|TٌK}X1tbX)P\_K 0/^=w_u 'r#Ask$w_GNR&Y`N6)\#mB`%|I5=|?"n2bfl e#<{|zQ-K/B )9dj#bn ]@` v_O~o;+uރroD`= 7H$z-Pnoo|5ɷ25t dO4dck^?fapM6`v f#&:=[NDD|O [Yke1[7n帟7afo]^RZR,a2dz3K4=Kfp;'?fNÒh4Sw8G7H*נdla෢C8Gf"Lh´Zi)& 'b[ymY3Z|#՚4050(^PَZR,ݗOwc4IV .T:b̀❒,ɚ $n['}>11Wi ԏiQz` с8 W8lTn߆RT- \(3[]՗* JH^g8bەJ;LT) Goy#-Qa1%Y12WXB㍵E:y]U*d%_A4{KSbUn4|'}T,^@qHχx@Vn H'e.D\: F KO#s{Bʮ׏bKD9j,-,k o^R\=B>ABR$YJo*y(ƾ'2W=!Yh08Z7Po 0 ˜mrSͤrae Z2 endstream endobj 758 0 obj << /Length 1843 /Filter /FlateDecode >> stream x[mo6_*1MR(]K:-&-RŖc-JtcQlKyYMiW,,ݝǻ9yyvxЧsF(Qy#}x8zpq;?s x^}r#f?Dy+(: E"*B':J׎S0.R:X֧NbR-m:didkw4>}w! k,8hJ*A$0)la-z^Ĺ}l%ze'z݂c8@imcW"Mvisou=٫#}_*jj B; WϏi~. Y#yu %ˀ|K GZ͇\y?e$+! mk,J +}ӽYC7z[9m^XC$1E|9$NE0$_b)x(=z4y\GUƧZpOWD8S40,X Tڻ]X6i J &9*(5`KLig#X ȄFy\Hȅ¡dRJ+s5E&(әR39Y :K qIHJRg٩@_cQ %0@LEO: j<@EkWJO$@ͦFUU4v!afgIvfgOYiƌ74ٹm1u XWaDEۭ^m%$*rWP˃nhZV n{W+Esv~Z<^&Y`[ D_<-tZNn-xzz5k Ŀm؊* j:xy]BKD>fXK=dJul΢v?)vjڥgr (|Wr}fMFTn?`LSC.h =0WIݿa9nR=Y=rLT&m5Ck01JF=`h&9\A)EG:n!IRtQ}yT)p i9T endstream endobj 765 0 obj << /Length 2247 /Filter /FlateDecode >> stream x[{o6?Bâ6IJ$b@{Wup,ӶPY4")[Pfd=p{?"qʽsBDw~\WMΨ'(f+:"LƕOW" l~ {KȏBﮝ3~ Ȃ-f?SBh'I8 VZiFŮ,")JvRL̲x-j[^˵_gT?0_N# <Nϔ@x |#E>J@ Ǥ`S'+`-Fz!F<ˆó;>%N/k e!">[N~hQ 3dj.wxM|tkzf2D|7&,cޙF)HCC7hLؓ`Hlp1|@`6Pl? c94 [klA>*Fئ̌oZU֯زȿpbfR}Zw{L@a.ӯ_e#_b!:5gzZ]ip,SB0OGday1 Nq纺P#7EUmO@+ NDܣCQ:Yr`#Hu(G=Dr#U 1F"#ɨOC^v|֧"ʥDVȂJ ; u/N;l.eU-Z:Ŷ}|#ӟy? J ";7m:8Xpzb"˃)6=a1Kq XCdFWLt넄=CA锰,&q[ r[;I(ej89#n&~bf`# K=?88> ͨdҔi}`TU D1Bݔ ڸbD;ҁ bp]絛P@RYIzUxV)l: \\S9aoKY)P8|R)͋Z*R/*SoVd&+9B,ߪgf!soM⬑©$PZ+|Ucv/L؎7>R~^-:Wch0DaBb]"^ *qS0XɉpEZR64e$n r|a1;^QZ]KrZ^N9ϹkYZmL/vglei1NC(Gsh ;HuM&Z `2R6)J461(rTaf6Lb*>tF`fݪ`SI.6ӳ7N|{-wim4QdØ@5&8˞L> \mWnQ%&0KqVxyo,d,9+Wi%ye=+?q]bJ񤩍j~ߕr%dsM^,d}>ЦaU*wSHocY4*_19y?Hwﲋ"^`T,6MkZbNZk3pUTW8۟z~ ֞-v'Y*jδLc _lWIjNEK7|msa2Dاiȡ7M#stuK}WxfPR:6\{Sۮnf2tG5/J^_;#G+GƐk97LP=SD/XZnݯCmFJ-.f07b@scgoNɄaĎSq|o٣I:'u"J?uWoD?Tm?bFXぼU0ҧdc;w/\iYQpA ł1rDC ] + S}$Ip&ƟeA'9E8.^ذ۫66t vJ?+T/̟s)M41ջoJsy #dd\5 "kŠ #stL٬Mt˛ֺ_V4)! ԞAk\sиDpw?-+~~| sM#~yDј?Q~O#B吹R:E P R~Oxu endstream endobj 772 0 obj << /Length 1723 /Filter /FlateDecode >> stream x[mo6_`1W*[+P`ͼOm(2%Wf~'r,i-ɩt<򜎇GNϘr%O;<6r1q1F ӝq0~,Okè^洝0s0&u]s]-SC"ܟi2c]|b}ɖ8IuJlpk ?!M8ۧuoW q`q~HBXFqu[$Ju.b6ܿ"ExDTb=":KS;{p768}뎸?rUܷ=\^zF7 lwDq₸GT,N-,}A$S h&R'?LUy&(o4w_FY\*0op\1I$VQuKtk.r\ 1{ՁfPUA7jcz % L&_D::' ݱˡ[8-h5O|9rE_<+yq_G }&[엁" [@Ͻun.?֟rFy9;HtBϴro:\ٰ=xHO?kwvO_ \D\[ylVo֞`ݓ1XgN6o٬lqXIH0-֦fmF-;* \+ 1ʭpR#.Ľ`f hbCI㮒B- KʽiߕPpzV`>R4`3weA¢XJ3eo*;UxRyi$e׏&e#WxS %\ܿ{V09t h&"l%JEխ(}Iμ/I3? X IjyO]ہ v1798!f$uv|OBS~Yz%8l֕W @r MeR:;XhVcE]t%E-Y9u]/ޏ endstream endobj 780 0 obj << /Length 1828 /Filter /FlateDecode >> stream xZmo6_l IJb+СKb"Pd:Ge~G-bmZb4yw<;v^~fn$8|鸜#P י/㹪ɔeUʰ]DJ/O>_~nF`8&!N_;"pnQnvxWV O0®oD@)7ӜSx]]%Z ?`o~+uf4oreP64!l,f (dYC5&mؙ ~^@U]DHqX%q?/I#Ko6ks-n^JRd2Jef V'"OMKhhϕ]^`:@x^UkN |n7Ê6LU+Ӹx4 q3~wu<Yz0g$>|m\X =р5ܬ{w1LtH]CWCAMU/GOӛJwO{Pr:*'7sm;R/gٌGs.3( 򦒪<\/(AZ @gAki] LCHu<jlPG2)lVzFaMOe(9< ES\3&̆փ*Huq6)Gt3ϹWcq_{ž,<_@TpPWPAvpa1Gf֣=X0DY@R%^]`/ ͽwB&#e]{]v!w[(kWqy!JÂ*{`P'2gjPt$dS[#~xGx ץU  V'ϰ;;Ԫ[=pQd:=ls rR{cX6>j:ZY۫qG @TEXӻ#~ wڷr{EE{W'0Ey;qX}!);}9 n Pڝ&歯Nli0E A89x (lأ9 pԄ-`07΋-g3p02^qđ8xCp6ˉ/XCtP8: 1f|1q*W2+tYZ=bw4mQ"r1Ne XJ?![r3P^O83f,:Wqw 7U\HK! *ZuŞ$FN8zC^[KbSz,2|nrr HA|j!%мMFBJTl`VJvMbTEff q/d"^j-e{i l| %]=.ڊjK܇LP|q`UU@uj}u8Jb2 jk3ƴPcc7-~06Vu?wdF2dJ2y* u,^ݎ3v5-m<74TFBi^4s[(uבa6u=3U";İwRW0Y NaUYf@aMYK-H5Rw,5Zi1h]P#`܃1xK EB6#MR+uQ_U8DE7M;Rz_aw endstream endobj 786 0 obj << /Length 2639 /Filter /FlateDecode >> stream xZm6B`q6kEJ kák?lV׺ʒ+Qlvn-`͗p8|\BCsi?$N~ ^\]܉8^ 'Np7}vMnxh'ʴYB&2+ER M?poInEN9sz ͝^x^H/女7G⍐ZhEpu͂3PgzIY`M2uI`d΅x2ߤ22Sra6?u_uYg6^&vReQy9 ,e&JS6ټU!$rceRLUfƧ)&iX;G!)Or3dݑC0q}fǼzecĽͤK. d%*zϲ4[; n jI]kAVs]S.W$RG]R:3ڐ:Y*cEBZVeE ך$w:ð=Ц_rTc,[d67f0|w>ExUE-ˊu#8%"ȡWqzuUBI Z `ĥUOpjTR6Te CJ#CSڢ%fDh=|jhba.JR<7?ta|eqjt`MaqKb}n6nN%`>){p_P쇁Vzv ff_5ǚjwRP*6ߡu&WsHZnw9[qyvcے`G6B;{.[%iXk{h<%Sht4s@]Ec\{ݹ鐺k˲&rKA-|y!4a4Te"uI  +)?1v@NETYR۠8rCwǺ23Pák[<+ 7>p>^ש^H!8F)t理oO]1hQթ𾏵ߚ 4NoN& _,\~Ջf!R|S7bh_=QWt tZre2; 5hÞ/$ =`A>0߀flLt2}%Yn Uگ_Iaڕ@>gc/5paz ր;ٝ<[eRT&=xbЪ"‡1Z7d\@Ѹc5#M=գE= bW!,"AmKth~аK ?h$TFv"L%zq]kAPTZ )C}-EYtebf:| o;bV<1dR6,FZ铰ô)RYmrAٙ h?Zs'F+J>Ocʧ.Zjhlyz gsQ*gˡo./y,fze%&c^,:-*xy*2AB|R9h1VxI)R>˧;'KJ4ǝ5KkM̏otr{omEl![шOf5(.Gvja3mW=D$=nIT=PM.:oi~7#ƐkY*ڝޏټO)mNP۳dz}֦tMywuwʂn|PB7;uY}S͑QNNQU/ii+Q`|7vu9Tj Z;y\icOcϙY sZzNQd 17j ̥1F$xQy0Wn73ԛ|LR!D!sI1>6tyiYcS[]}/ϰ endstream endobj 792 0 obj << /Length 284 /Filter /FlateDecode >> stream xڍAo0 >0N&n:PTEPд?NO< C'֢Sh2t&ySSXڻQn˼+LJB\<(pAGF,Wkρ0N=| ] Y~Ϻ&CO!i̢{K ]|%"N D!y3f2yBҢ&"EX!?(Ck^i;-hT|ԢeR\]z0TkL?fm? endstream endobj 777 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 799 0 obj << /Length 2067 /Filter /FlateDecode >> stream xZmܶB `T[x^Hɰ 89p~HWJH;|nWI}R hӈ/3ÙgCIy{j~&^RPjcQ4r'Jvn X_Yw⪨RKaob/~j낀2B 3/;,~{9awkF&p--zq4R`Cf2>oE%I|ezIbFmaVmVZz/Z욦(窧`% 7K[߹rc̫o-~!ԶX%T3ױmà %GފƱ׭]k&,3f`ĂoJ- k֢R?qPvj=LV7EIe[nDkpMknza. NySۏFҥX5z+SSNLkVVxed v-Ҷ%ظyz#0J޼ϯ>׷._*+\_pMmy=$Q SD"<Ѕ+m0 IVn,S̳Ki0I~4]{?i藎5mjҲvmtFdNNRE $rr5‹ ?;0ٓ>*k )*iė ն( WН܄O39 paKԒDSd)YָmQ1H(J70 !>p c+܆9J 1E~zz)ul MP_gh.BFhﮄ * 3{A^gggr" CR,s\k%_d@8֯ G CџRD0^'+V%;L]im UG]^8`S{9˔IQ %@Q3TKw&  „+MCV4E3~"l3\Qj bǍ' Va!V.Ch?@?-5Z,qФ)]כ戁cXbks>d-p׮ۮ1I˥s e Ր1v hsfĚ`T[|umNv*Bݽx4g WcSLt%, &WwDJ^k ƒG> xh~%"B`H.\NHx~, 4aDyK0U4]dɦky;pY{`=%dPGB?ݞ'nDyCPʅEdEƜ.@׫X9OX;ɬ}]FB̎HS6c:% IPόec<}Y;^8Z=Ы\}%-pOL-P>h>R_WAHPߊ<^5;wcz9 NϪfVH5djp9ۄ}K&ɓkk4ɓYA?OT 8yJ?Z> >> 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 805 0 obj << /Length 2744 /Filter /FlateDecode >> stream x[oߊõ>(HeCaѢ ɳ |fvNEtr9;;Ҿw7'89G^LA@BʼPDDܻȼWP^Yõ*ۤͫ;WJen( ŷ'_]tBaߣ^$ KO^{Ǒwg]{"^ķGYpxU_O扟?Na-naMqT7oyklU{cUE6I={}U\9g,RwQ;U/:_M[s̾ra24\_}3stnڡ.Yyvvf=s&VH?g|9xu}MiU>rS}WTiR IE'ρ!#E9 rq@"= ص㔋}աy.scnxº5/"5yHFGf5M[!lIt]_ah9ic!pژRD)Elۘ8JmS5k͜ṿPתl6ijkIt(>}KҫSTt0JJׂӷUm. tM?R/WI:YMlB"b׳ա-[1&=5b˛m`mcZnW'[YGj*tG[[ Abi qeTyD2 /YKP#Ba2͸f]bľ7fU.)PUaA~ wЙ6|MH.<]VP0,&1a93ci~U]k_l4ɋdS( hCzd?A{UyO5To͂QA/\ *-<nYs23o8߀(}|LT8mq?&"9.H8"!8p.vR6`i5^JeD$t#O9aq}@Ca; l$Ů 9SrVd]Ljy'ݚFY}a8tqz,$!5$d2d9jcNnо8\jݳhܘ!c\^B-8R'[fR_ŽnU޽F._~o3Q4V 2Suq&F!eGيd+a/4mUܚ_p#ߠc 2# hG*x:c}{h C%yǎ)@U9˼DaL- _1)GG h&o-j00x">'#2i~]vIsCb]* v)U{8)g=Avr_m!>Weuu=8'fD;!<"hX\@jUv }I؀&mMLC"T!! tU1yt'"G"xaw#c㻹3UeP7t+Y>بF23VpeW[@0jH#`HM1t82vmvcN4Uy_Xlw{-&;`&f``pVƌU;kw'q讄Xv9 rjDU 3Z#zaUJx9 滂 >?OK@B0#zi F\@>xo* endstream endobj 813 0 obj << /Length 2750 /Filter /FlateDecode >> stream xkoܸj|I^Rg" +-;|H+rΖ^ ,jH Υg,pBw!s+獻(r6"pj+2*,}d`r#B³w,~= vAɑ‰oagX8Wz~ύoؒdzEf$݂`0ӫӎ|ҞBN/72I=Gu,M!W9[2/ln$mM#]YnW3()U0=0$lZML_K\Jc2]ܒ<'g7` MEt)bH3ӵr U`- ,2\Z®rmZ}vd4bki^>ΨkfhHm&g;hF8ArLj|mW iߧv)WTׇdb Q :@=ĵy&8̶1yx=o}2u`zl2muI(n 7Dv4TX֡嗎i?CUbE!„8 ؽj炡^ ɥs|W߼'Dh=Mbrn1>"00] Bq|fPw U jrPeI{crj j]^ލ\,,IsbV{s>DjHGih B-/l' BDD0_=~"h7AS<5?VQγ'ϟ8ڎ1jM[dR ZA Zp /c@,b /xD:I?VNոjbd>GX  +: iPC?N<=ؽ/@0^J_^7:C"xSweOkCW@ v B~P xxoKc&5=3D` '@>O 598tq U"0'F6}>H8bt< Jދnz߭fS'4V;ʱ6GTQ5Qa^nA;#Fԯy.Sg4XQdH%BL_5U߳>'( w.7A5d)o'd N[s}f|LyD"޷n]a:ۚxS]TW+34*:H, "$:þzQKNJWRm^2_^ϛ|V/.c1]Fe&b 4Zyk .McqkVO˼#{ʩЕ #GR8D 4'kO͔e曣{e@@&~Z|Hh4@l샩}!Yw }ޝ,_t#4v, \cU] Q+ӷ"v= $gVɍLE<i\1njڲglw*\(lpe?tȼ0a2rU/UchdDqp9 hL|&s/KEDsDFvYtm@ϖ.򨫡jNB=`xŚ SU'k*2s̈? #ߵ3².Hn5OI.bq)9iL%TN1@,looXOvmNL$WGG<j{ u,߲$S&ӫu}Hv=-fZfTظ2Y!"-wDpk%oYncTcv{|h3Hr#̑NL3IBnMPz2_î(}Yr~݈* 2Y+`SI~Խ[֮ͱ@̕tܻ;SCX\lC̤nrZ/Z]}k(!E+tv30,h;C 8c~;q$Zkcdr>ǵKCӁ TvWJ@ɶSDB'w2Ӥ+EoI'Z\fћk\C2{sx8=ew(W/?]p+Wgߙ+f#uzLeɫz)>gJ֣-bH0)b2> stream xYmk7~B/i!Z))i}#N ű/[ܝi&MqwwJzyhN] NR \kв';NOuDtvmJ$Şӣ䊘~vG1eGd=T% T!POd!䈳 HsbT  rbM.%lTĈ%h,]7I1r.悯D)jPԮ+-Ӥ1W6& - Bu)a+FfU-KbhK1;Hc(,j5`.lWѝZKv9ª*e,r~jBrY̶^UCbPv2\ń8TGq$Z( 4gLg@0wSl- MԸhbSk &%jrBfXȬSf2@`'-`0,S@sbBBТK_ze0`&ۓ hENl d,KGG+wvO\Ͽ8 /0/>q=Gj뎎\tM}#F_@%|Ŭ޿ `l Nw`k}YrN\W\tv>,WKtZ.XrnEdigMP_o݉5XsLsƷ;uzb!j ~}=L)NvnfφLِDcI^czI/p1H3G>>Nz}_vocTսx@:xhWrp$ӈþ*T}r91d1l\[G[̸ȶ}0n ˁzC1Vϵ<@-̓nAgy{^m>om^nrҷWf,H^>!iD9SKҾ3!1$bhHA[wz R)1t6HHN 똲G<+8,wU+l_-͋uf<=~}"q41HYqL'׃f3"AG59 8i8 Ξ9 bldޒy62d&Wkܗ= \yxs 4O<@>1OBv> !ِDȴF4Gw,<0h%MV!)qCUt$h[)LpQcJf>wIӳkF)dȔ*\VN N(yAVhoP02Ts>jdN\"쭼l]/x={ӫӳ۳M}h޴WK/%Qq˸Rգ|ٍ__-"as}L,~qMprK_YF:;ՉѮN};~x񛹳)>~)*PGaCXtnzU3eOȠ%&OFOM!~zGN&+;u&uT/ oJrЧYFAY5Ž0-% 9Qף?2NРzaLiZ}͊eVq1 endstream endobj 819 0 obj << /Length 2927 /Filter /FlateDecode >> stream x[{ܶ>ąr)p{wv]؁kw>>\7 '.WÙ g]̞ Q(p.: /dy^Ua2~>JeTYDF4|Ӌ_Ñ|;۟X87s~{-fa Ξ yHwdK=RT Kekܝguȅ啬R-Yn.Lߺ*My+EZfЃ.e2wľke8«9g/̒rKY Ao"1=gJ 97CeF X3E;QAqf~?@ҳFOo}O߯(T+ (A- 1D  A8w"av[hpl ilEBvqABzGP"@؀hwNq&E\RaO(1=Cw`b GycFjxcrpx8SB͘T#JKh-DwZBaȄopzAqkBi@Uzťyjd x MY}9RhY" 301&}DlAB:b-g̀:x0&:*^~mߝ6 ò9y;qvR#&(>vZ]Ƴ^-;*p0[(~e7Y(x Z,j|BE6*R5*lkA{!;DQmt19C#a uT{?+W:jI\h^\ GSf;jZaݯ  wͅ'Xa )gc଎G3u~Ѽ6]s .3;zo2ŚD]vmF[SIԂqZq\K1_ n@K3`OWgrĚh&d*X #GQؓY^[ɛ=cyfsEk\DP "As# 7,^Xg'(ĺʖZӯSQ140K.ܦ 6ַXkPb}i9?ĩX >wnr\ouKʹ68W+ׯ H}2"UjG RF0v1*Tn%Q=-&6<^_|`gq6E;mU}i1Gjz-}@B#k%;:%^Wؽ>:|;7 F|CfTO:mَ E:p{aPHu᫋,~P @*٫ JSB?Ҹ낰~A@Ԕ~4GӳVY_>>?OgOFBiMwqKu]@<#8eOkQ GuEfQR\!H\h 2ʚxu.n<*Om]g/~Q-Јq.eB)n1^ iEվ Paz ԅlzX6e$#1uH PU^^倩X:)1cM 4>%::Pߌ(n/Z/2Uœ.vצlıj"Bj`_.MQOlF*:ZhξU~ժ6|گbX7ff"fQlRbl2e] YQ.7!c#U9 X{ǡ߈iU7x5ƚnA[D q#m191KĬß[SV6wصxwo9rG!$Q?рE{GYn7JϋIMռr]`71C}0Q6{7$zJVxvd"AOwvƮුkӨwl٪bLZc@.&oH!;(/dߔOݟ(qڳßޣ޿U))mZ_Υ-{V*Ed endstream endobj 824 0 obj << /Length 2585 /Filter /FlateDecode >> stream x[YoF~}ڌd $a-%"<oS̨Ol6.ag`ſ_ QQY;>u| 2gv~-:f_:YUqmE"R|/ry!C4I|;߱oX8{+u5q~zWl}ДТot#{!.貝 ~9paC:+'bo֣Nx#XQzZ/KQ(:ԕd8>h@H{"\uNܜL>qix&?vץ(Ъ|f`-8+QyT;*6/{|~2=g G{΂`)hT °a-Ra! <}aFXP"SX,%Lwȸ #]wpHE0or61t$\ I4 }$`oy>Em Ɇ)4pߵ|=#Wڷq h1̈́ysM>3X(WÜD(C<s*%6 AgL]ecROPXy|!'$bq+2!9|Sf7ţ/tnQT/Gg1@$`DDZpW?wlW)8Kݢǎ]"^薶s!EYrI`OOlphqX@|@Qϗ&SfI껲3UQOi6$FGnY;p1AP=41)5F~ 3LBP J t0R^m_.c‹ƅGșZhB G!3o2-V:wyoќ9`*5P-Eƨ(b,Qimv"Q6pHB.uߏF4'mtRaDB@]P;!ӦVZ^Ҏ~jP XyI`$Z$FT0YEP7JP6)ArwN$ZE&̲WH!|%qfۆ(&V>N]D쨏C4N זL.CO4X`(vЏBGqu[4j:%!˧8N10d$B3e>:Fo:MMX&$CGJrzV.Ub@2C}3bA}V~J[UJ&Kj#)үEA6?(o GM˓sQS<!f LJi>׷d"2Q@tACTcCbibm(K`/H/)h7GGWTJ <l>y./?Q֧ 2oNZڣU`љ[$FY9'RMF g\G1 DrHFYFjU!&"M&L{4B77~~tg需3`N,uɅ=#ǎfM9։Q+FdBcG*gvGw# OT>FDMD̄_y+sD֫&RHʁdnrx }0R08=SD_]~3s!7[J#U0Mpr"/iv"*v= Y e_/~湏ΊR?9qd!Xv AKZUr9|R!M9hlryr9-ؕ! ,dq&rwlk0> yz z`sz/mxAjxOjUu (!hkԑ=Cgj>ѧ0PN zR:"8h^_4JhrZ^E(z줱Λ /u{bCz«, b~<%(a%KѶJg8|9*K. gH|22Ql+ZBYO $( ܖBftvC#z#.`إ3p :W(QGN: . yl fu)}SG?,.VAe3^tFz[ c|xuu X0BWUL[5vJq8:BU9F(UUtW]0#f?+N/@T)s  ?U+E/'f4Ɵ4QMTggnFnP j<>s ; @&>IuPggܭ'hI `> stream xZmo8_=Ӥl.n zCZdR*I_3RPh3 ŝÝO~LȉY3tdPNE̋3KѬ ?f&i8W*WI`!㷳'f'N(c!-wRp&ȹѭ֎D̝9gn u ^ܪ|o68ː&J4Gvst@O=IwԬm돒|ce5=BB˲ZS:TY2ARm׋BHKoJT q *75%=MH˞/fJdjh!XVT ~SJ$M.xȤ|-2mzK#ZouZߟ'Xwyes`B0lj`~`~sN-(dJytO07q8i]| cܦxhdf΢Uf{*X0~~ 䐽=\ kj}po/6 :ߓLD1"@@H|_|aBEYe2 ET#19|rS[$>WpΚW0qܧNP4J-(t`X*{dĤɿu']VQ(qB7#U$IRlc2-@TA@g %|3gc ?woXa BzLN` и"?WoVt6q12 (C< X@ϫq82E.p6{$~V8*=Ȝ[*")|d@mk3^T*ڼ]%5Uϕ**+ B&3F"JAم4&GPAIrKc7 %cLUS.L#]^fH & h3׿S*,i~|TgBQ1k̳ ujRS('Z-PҥJMer1&{FWͫ@m {kmg#}Mzy +J?6 uLZ'No%5-NDbiӸ-z]fky9zUp.T U)V0OZ&iu~G Y[̤$1X50}W$=['b՚kkdvFGsǬ7B'NR$eg@RU#Bga6OKU fb&ڙݧ,]܂ d>=d4?c8-UJ3C ]llΊA5M)m4͡_LGfe]VMIَiG&tFHcBhC:" Ex7 -J6#MgB>m36ːuWaM 1Hf1Hlw!  w(++ބA [|("…]eTZF>3HYqi.)2$vt^&' \G`#8=K1j,%2 v =#x):Ej(qܶNha9}#J`u5YWnLHEĎq|94{Da 85Vm-)'=pMq/诠,K#b"4u9l/5 PxbA0P}4AnTn{Nm}O`(ԈD |^6g{, Zf=/4ͮ/ܷg>31bb*4K >RI{E'F^U !娱_a;j#W;3y ;x,@"`&Hv&zkX8),N5Md,{h~ r0a0~(ZZous 9k[IHMhth}A/I~b.pYLJ ݝIw`sBŤaHv %>ާiKcf` ~L\#;H~9ݱR/{ k?@ endstream endobj 836 0 obj << /Length 2842 /Filter /FlateDecode >> stream xks~&Rc @I;ڲt#k4yP ${")GqьpXqwޥ?~/D;|<@RMC_-Zq9m!xS?ܤg<CA")yqEIyCԎ W= 1';c;N3OkbVd!„$Cs<"A˕w1aU%OJ:J+&[*^H1H  |#)  YoBuP7 Hh~ytÀdYH YX%d9aO˿#|C뵴Bا*@M-cBp8Mp0O>#/h &r7:8.HD]9()lӢχa,+MC(c a&*sDC~(/suf.U_!NEp)lj&t .V!Y8K웮KZTi^<7,::o([lIu8-i_("|=MBXn$1%RwN*mYhLԙXty-EOIˑXd"&n13kmcrd7~}sj2cwWD0dL MzfO<%ݒIo#b$ȗ9^sR$n<%d)=݃N^|sj| M:*4,o;#):; ecQ {-EsWg&e0h[GB:9`cOlSk:*_AYfHwl?vg+sD$߰δleSr\^4rw\r&8q_n{`ܿQQ";viahd$C*[Ф"I7 kd{$[:$쒆:ٰRcF\yW/^ۥx[wA8xt 2Gi%tVf5! _gQNzF.58˼BƯ2SgKߑO83aaG"6݉' :bf[gݮTvĖϝݱ},h'?WW*dl+GX>Zu,Xi鳍F>gDPt1zropfp .z0c_ hpjrar\}&'6S!l=]I:0_PWZwx^ۃmx^/ϛk20_LeKhmMG B7nkT]ZܔQⶥR =G-^H>A3hR%ES*^Yݶ̲ CMߟωۦUTږũuUNRi*7Mߘ"0;^=82|Jo`.Du=wlx2X|]fiohjz>,qn>Z, Is{.`"ZK'פۤ|l|7;@|s7q[ޮF]^Bk#=(9-$B$oր*Zwݳͮo%nnN >0w*H GO@"2J_;qWpj<+5@z endstream endobj 845 0 obj << /Length 2427 /Filter /FlateDecode >> stream x[mo6_!.N!/|赉QH C^^!Z"]ǖwf3ÙCW-ABIepv0)"4P\#,>Ί*/ҳכe6Y&;vmLj¸)翟?zsv>8 $9RXѧqC#LGa/#|x4?hq/{[ve0Ijv FZ 7%KQrIVo)IA[mV*PPH,7cçV:='ebWX"W‘ v#1 ;N~x4`ABbaQnU׏@y"F*Oǩ4EYZFV{FYHgkS68޽9q “SFCppcan+u?*0k;$aC넆C 7Ŧ "ZObHE# BmCÊ!3V>/$q6~ؗ5 K<@)9m6sgQnN !ʏIYEX Bp 1fJbӖh| ^orOz3bvk*u-6ɲaQD)\S|ScSe,<Q(d>U6' 馚/$eP{iv),ݺUz l*v4R{Q9 <@w'i˕YjGX&ϢԵ|ƘrnZ`1B`a 7+<*]v+NeyO `AeQGcUyƒw > as0XQm-6U4d;4*a>X B$1ϔO W&k¬=Qf'M&O1V#<֋B"RS$ iXV_dhJ( `ba$GB1.(5E2yYWrm1*K 'd`'6jB*[ +lLVDhP C()4@ aQ ] B. F:BB4h)͞%y: sfsa'@6 sTE8| lt'#6Y&M x8xcZqJѮ0*؍~źd=DO^ՠt~-8T( VAV>1,qӤN?}?)k YgJbp,Ǥy޻&P[gM=;8 yce/Lmp'A|GvL})G\}n%jDIKL,]wK*ݮ:#Tp$ E;ChǯlțPŇ]:L.7b͆}h?1bB BC=t)1 1"_8r;w ,2qLM61:)!eh;"<)%`L2.̝1a UYQbRffgـ3&ēoR\~ rJLQ2JEDӫMi+A)4R  5u Snes n (c cLnWkW{4\3^1֮rCbeMh$_K&'CH Ҝ57ꓘQ˙W2D_qDBQLmPC x:fJ|Ǯ?jO,E?uAXEC:$ѻc{1<ַkE:*`+چS3VԷW:jiºgƒx/!Ӳov[{ C:n޺gt}n9p2rc#/mGqmݰ90f Z=Z@eKV&Tx(}HoɃo|L콖)<䤐H!@~/kٲ^Jޚrnv= 1?=U> endstream endobj 852 0 obj << /Length 2619 /Filter /FlateDecode >> stream x[m9ʚf۩/wIߦR˅H"F! k^\ tcaoaﻋ?]]\>gPP]mXz؋a#޵Y|5~vazv;9 $(Ls",0Tx9J1"LAx"ύxNz(VZ@!h@b\X>Tf2j;r\`Co8y!/|Ĺ *?kU _ʳ"w_Ox{%0^l|?u&Y'̾iz^x%iyY7)V[ Ei ,$ ҘdX qx,(Æ;VUD|}%ˢD7U*@@Diw]ʐd|cx6pp5 ( P EbDF$ Y4@ bH$?BNhh1BcM~>DKp+QCϒ~/Qa`h%勨LTVnbU=EYlE^fPR% l)H:t 4WSA%c*g)D DŽC"ce̊!CғAO'ƬNșYsh4xÀYZ Y\!)&AV^aLl>]nuA]THܺ$*öz9l*lY$ƍ Fnp "~1eQsyа^;ϼ=&FR"Y#1pGq Bt LA H MSG9q$HvfY&|Cg@j-U}L HNjB*Ui@":# N /uXPB85]. bOFH,33#ީŻBP1,P6|evTURT!ɥ?k $c֚$:)}Tw#Fcr6m46= ֭|R_ЄD6^t+&Q9Nk7K΍`Ֆo$pk.D+mǡK,|oaH]gZ-*y6 fD<%R")?՘GUE^'iVfRmC=(! F'aku~jQt.Ε'˻?c\7AY_w$hHhSI'Q1q*ө!}<ז5/EoEꣿ&B8-5M& Ld'f0,A]or夻wF"9gadwDlW^pnnJM!DMԾFߝ=v)"т%uҔ :zs'uͨ Ȧ6Z_;0H$*xFxriizbPDe+LYMT酦G_ 2j#imY+ďZ'*hևnU%HjO)F˨gHAExP]Vc nAB" ovƄw> .oQv5usOPxF* 6Cn*kd;;6th !\Qw66=$bl6O/TT>M,f ՝5O H3cj> 訅JݣыWĪFIܼi[ rl(z }fgyHZ9Q{VOlh|z҆E;; 3Z0 o8 (=AOmI}~&+#$HkpaNBw~nE0[C*J*hӘgt3/ €Ir US{2=8xKΝ9wvo2/?sL1{ WUK}BN)k+GT|UFFD \@TFM6e' Jb}p[atO-f:JӬ,BI endstream endobj 857 0 obj << /Length 2395 /Filter /FlateDecode >> stream x[o8BW ")=p^zlvlѶPYC%Icɷ{DQ3<~C2Y;xw*Ä@PGz>\GOuQٜJK*-2 v<$rgHMCKg=D |u<5q~<2]vGls.D]{@+g<i@Dkq-WV:>hSxZ>ER8{(,w~)4<̐iK:tETuMʓwGS~L^inO#V,%p&\J;3'CBzpa|;]RE٧xPfڳ#ܪcOnlҸ"?!s$ x%x;>Cw:aƒGef`7 p.܄I^ˍ: {\@H|GmrqI~qZ*,A{MR NHwpMPiy ^w3DLk%_iљmdlu,MLљlwE3k/VQMwcxZsܫeh྇I[f^Ð]:#|vp沪VceV) tVYn_*u.#.0jF{nwZeI(w:E\f *WpmwUanjFV_Kc,-ԅ}ZּC#<ťȬc=[=@ey0 d~4 K&l {xu\&Ιt:-m;SFy:/JC)BnlT-HYDэЇT &1g*a  ʶyS>dqtHEr|SJpZAd"6Аɿ|;@WG =1AqyZOsÞSh4`db8 s"|4Hm좍<*.zY t(ki(C khXyQ_P$#NmS7lszll<+ue S6&_'Ƚ4Uoq=8teSg}l5F8A>A C"¶1N6͚ #y_4箭S`vYQċD J(:1.[u(#rv0#Zay++b:0+u_ZCYn*vy ZP_kFjh@s$@'qϯ#=9k*qSpC1ξ"S"З&}n  3U_:?LasDY0 ?xBT7?짅P=̰jo6 hCo7ןـq0Y+ޣ\KݰħH=y -i-0Γy!*סgnD10{-YȳjIE zz)_>U=dKdpQ P.m_i(4LN14%e5 h7;y1UdjPSkGЛsߛW{LKlTEC7-ˬ<>h6{YلԔs}ЖK.T:6B=pϏ&ߋ7C@Z*϶M%RƏ-SgX%\?`_~~|z)j8ug_ x>/[/-o&~5&vnlUڥrX5 iUP$&oND 8Z}f$2 l$G?^^|qB],k5Wᓇ c/MG]8mlu OWf6s|SWp8w<vh)h_W}& "3ׅ%`f]fk"ZwqiגA;}J:GZݡs}2 0; z[=׬ ^>l O{R0W*-j#PBz GHWR\LW p\&pA EJ.o$PےCsSW^ҀPOv endstream endobj 864 0 obj << /Length 2474 /Filter /FlateDecode >> stream x[m~jD }_2H4m$ha%Q'”(w}!)R083+y{uH*ەxۥzrۻ*tQnmq}in$ >۫W$9RXy[BoF,{kq5 'x=fʱc +{͢]MYuXGgI#X1c}wۭ̎1VK{)aJ嶈x/1y7$aewyBboz[Zڐ„x"K5۹b(P>ɑľE qLRa %qaXl8uJ0]ɝ:e uqzs(SITdr(a7d9 7& ~'ij3‡wvBÆ;2P3 f?c o?Aُ?8vZ(/=#Ɵ^q0t~c0" v+@J|viP )Fۅ5Ri_O)L<5B0Ok KK*rHL!r]ҭ7>|R1q#wy0$m4&fv>3F@3Y_XȺP' DzXOHϐ`',ṭ,^~rC!#쌀=R!>+bi*5tvC*+'~D2X~;~L7 24 qC!LGU1 h6y6N=KBz(-`XF@d)j5=_¤zPZT:Zw*B(Q誌B-ƈFoeFZ@ÁnI4LtrsG'%`+LMFbwEGĊD^Fa3.{'" >^&!*,%$zhQZuM:J=iǵeE/k'D0R hS]Z {. <+zYg! !$>z8[1~].PNH\jnUV 4aZmtcmaWzfSټЕBrվ-\,,T? ֶuB4 =u_zwnH 8@?]&ç,PP,h(903k<h8ِ= ֓5%ܒwWx1n]Pƕ\%΍Rp `AE0lwN`n=pa,Ɓ\huD]58kǀ&~[+tQͺ^\_ j0i!M9?XRoUEK`|!U<^jeĒ@ iksF%D%4q&?hGX&]$qCY֠mQfY=n/-OVJɆAizMoZi`7wgEVwȨ(bsR-R zA)DhEŎzLReWI҇8`Ҷ!r`C}RQ2Vk4R ѢDEÅ>pL Bc6ϒÒ"ajaUu.]^h>!_ l {=cPcFϵ;C3ĩƸFb  q%;}{NeumCpH#ߖ!aOg`)wf;GDBz+)s&p˸9j6}Z'wef~buK|7` endstream endobj 873 0 obj << /Length 2230 /Filter /FlateDecode >> stream xZ{o6?B(]i)õw^6bӶ=\I$ɶlnSS#j8$!q p⻫$"`R"EhxxĂypbt^uR/R:{ H!Eapk{e!^]ߑ"ax0bcGq^pzLnn4=V1Ӣ,2Tk=K>bLg^ؤsX&.s,k\1@qE~Bf! +"D|=E> S.&H Mt]jQ:\TIXQ5%;|kg'[E'Q7MjF^도 2gܢ~eC5:Y0;׬Z]+5=vTkSc/n- "a8! (y)pJ>C0Q)XyLhF 7I^%k&ʵbo]ޏpƼ-J2ɫ:g-SXiZ6jLfP볡UL0֮wܼnY&Y^^V16Aq5p8,B\98N1ĩRöUyx~>.7zO )aځ)@wv'&CWL+%jxKt 7t OYa ("YiM"DΦ(9thjԦL{Wvo\ ZY{2]y?޾=']V 2XE#a* Qu_C `;&xCzK"7n[;w{ >_zodI1 sAFB; #ՅXSAϓ8Oe4i爆|P*h䠐2HBK$nkmХI ^kEr۟< k_ML,M.˷BХ8x9>$.e |'$Q" nNIۨ}Zиi I3 ).]{7޻XJ,QXe,;њD [A[Ɋ)VLA".ȋa1ʮXfeRz˜ёZbel[v^ig!}L5|Os3x7z8l* z&v;~K0$M~R({~suumf?Ew^lp:YՉsJ`MRBf0r#`G/r,=s)>8hWHDCʩLUf>⏫!E]p}rxM̐A5H=gskU\ogYU#⏲'(>DCxxN=VA5b`)2;P!DLzuv ;A >( &éQrlYEݣlmu0^ :)t> stream xڥW]o8}Wx+Ebblmݬ+5>*"' L{ 3ُF >> A߼wk/X &z$C2V8ZoЭn*`Rao6ixl7&7Ic׵Ga ${+A@GZ=1G#`&"JPT?.7OQ5lL"<Ru,onN> 0!~}!Ik&{<> 3A;N#pg(M@b)v=XG)nI7VedqOmiGސ!۹OwmYI 9;X*\QJ$mERYSv4_Z%u7-=|0}!$=/~[cc{t*iGk,.0u5ȅ#`2"UgQ宰\wM#LZ$_n c}aY%SJ/g?Pb)DudP~Kk¡М L5-)޽,lp%g8u endstream endobj 885 0 obj << /Length 1669 /Filter /FlateDecode >> stream x][oH~WXRҽ&<݇\$lM$V@X afϜ7/kZ#((`2hwkppoMW7Rg>ԞjO 2ʠupC$"E:N/^={:ɵ*=1;$3/k۔ )YӦ~ԉ#3wlB#ǢA! x4w8Xyt md6$˅4DG B?8Zx`/qӍǹEA ]u }hVbA|ĘEs[JDũuH V ݂dRCȠI2IڶSN׶?֋ Ue68տC'&FX.+ekrq}skpn~+Ⱦ_[D}W+ *Tr^{K3 odഴ$Y܁2#\F9>t94YҰ.3HɮRpmf6EP:%%xFH:zò!lO/[3 -ncnJ0-x .,v>300tV Zݢ-86Gd.)y4"M ,eʠ5`s8߫78)ĦjA`x;U{EcV <跉tOFѷ#>.3 #3D~ P &Dp){yL))Hᷰ+!7, 22& 0_NCUg?%q 4 ֈjq94٘7ylzrEݝ yNx9 F9%Z +;84|> stream xZmoܸ_(N xe-( IsKOWV8n~CdqN jD Ļ?VG'yA[{1b"jt[^,,NgUnU&cRZ ,ޮ^su-Gb{藷[G&ޥD^8V_x8x=4@,es#A"Ẽ4N1V+· Q/%E()AHS/&"HulN`3D܊>_gn.LYՖ!ǛrcGz)Oޒ*>0A| )V="* quuZgDo"{YMsfaV[7ǖV烋/^w6ReZ?A8ۜx+?qpHm7M;D<]*oQ\5kZOt e ({ рD~K3X Nh+[Ĺ>zŏ$H&+ҵxKg $#4q[o*b#F$/᱿LLYd3DŽjCEEN6/`ėy!эl.'C1`Q @%Ao{$Qyo~mn0bFn+wUiB7֍ΫJf9ث!  R 凈jFN* 9jpPDlG>\Ue/et{d- 䳯gW#6&/YaO:(jsh~.r|fN\bjk~[,v,gnT=^کh0hËd&&NBtN74J_SaE2o@USim0tjUC_wf>[}􌨜8uYl͂7TM-aYL`gvyY; _JUΚ,T1H.`_n8]U-6$fK6.h n PPCtn^[jiUu3P ٠6C6qdр?>'nڻ5K (XMkz%<){XqUIxWeh A6aSYn6h'|Q?7 ?_]P#1Aq0TgXؾǍHnjE'vwu:ϖC{Z.7.VuS4 Ѓ Kƈh,wAbݣ(t^&#ˮy;8=TZ=(Qİ M41b, j8I}Ufm.ӵl  ЉS֭, a Q4RUHA@/9 "-Tdpk qNҺv;Հ 8.n Bth _YW@]5 ~KoWkZ0ۓsj9s8dČ=ޞa(:{%!VHPTG*# kͤ;t;wS2\rWzHa^0(vתjkl̛۪uR߶},*>>W:5tU'z\NwC(dwKhM X||v-f,"h˟ML3A#9'Rm 6li6_ Ff]ݱ`oQ6|_QVՎedS]g ,s{{zxCw6R,bwǐ6rtp-1]rF:6;[T endstream endobj 901 0 obj << /Length 1805 /Filter /FlateDecode >> stream xڭXێ6}W2sI- 6I@&(֢Q-rHYi$9  ^,Wϣ,I$XQ y6\^m+fMkȽ,FRBׯ֋ rh\)HJ`S/nӠW%QvW$>xxNG:5#]3PO%4JQwV?v.E8ĥTK7kz QLC/uXMk"m JAUD{hL!#x/(x4h52ܧY6M5`:#ym[e-YX%Rl5PYl\|=)3Klh]H$KbəC*uֺ7SWХ Z?6Mojճ4-&QD }\\CNE T_vCA/t C)DFhNz `\H@BkyOx9 (' \ >C?h F2}טc]Y\=CO6kC4Cl (W /)|0LkOdOqjFtc;_M*tJcUv;b+~ b&b(H˸ge۰R3kt% Zy9[4l9'CVyf<j56Hn8y|^8|TC#ǦSﵻ $T l]wh]~pܲo^oT0CrLgeJME 5+핲A&À!i꽥 ;/beا 2 ~{3pf"Bअ|actuʊg|\jZn:5ߌ C𮈳eQeXxq^߶q`wMǓ]# Ko:͌}0f3}6i2f oMwwM.4,?#R%ܨ>QxńNyOW經9nӶ@&-O% pLC3I<O1?@N/nFb?*S]PE@;3OpWS3M[zq^;,'Frb 妈 .$ox?bO+~\nά~ȡNƛ;gw19yoڸO X~i0ڥmr#/b9޹m>};pF^N瞟\!8al x/|oƂ8CJFgF endstream endobj 897 0 obj << /Type /XObject /Subtype /Image /Width 1277 /Height 680 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 904 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 904 0 obj << /Type /XObject /Subtype /Image /Width 1277 /Height 680 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 1707 /Filter /FlateDecode >> stream x ,\ endstream endobj 914 0 obj << /Length 2504 /Filter /FlateDecode >> stream xko{~ _E[=b( ŦmwdIQr~% ir8g2۫?^ݼqDq.LDp;>Nnm(m׺0{s5twW?]1X̡$Q0[_}D9P"8bGb^.y("r4 'ߕv@^.0*=3esr_9ٕOѵqЏ4uܼQ, Qv|Un8 Nz-č#φe[0-CP/#A(^DU#_"M)raJzuQQLhoᄎm]Џϳ[,*nRMˊ3uv dǩWČwU1oBEse"8> *EcO;^! #181U*pY*IA.W] &JgC(~=NxOwk܂7 dXBWѮRYmYkNe@$j[YW6لm)V "B<(d4=+N ɑY:bˊBF5GS|= &*+ ǐ] O^N~ALb,XTg^dfUnkʋݲJ7+ "=Vl^Ϝʵ ͭŖ^`\WޯDR4 =祏T^G{=㏐D (9hua ݍ|~.--鈅~?k֎8ڤfW4Xvzm"ڽ OP+ΟOf#onʼH .h8u6V em(C`:!;ffʢB>;<ζZכqFM9=n{}ti *}6#gõUvI"(uYd^f* uK[ayv׹^#61C[67EKi]dUEjk6?&Z9K^@38q?mΩ-Gޜ&$f;[cxA8 "ڕUe쏛~!֝Z(Wcsp4E^ʁ5B1bvJx^) 5cl:sap)8ILjt|ê%ln StO} Z}w ^"E;ab"MS 5 l@W2qa`L!a0SK@ #ܽ[?+d7p=!9Rdm-1.Xy.:0 iL)xB8n׫ʽQ)p5иW/ze}DeD 㿖VHØ=AU6/ؙ"]1:i3o*;} p5/B6CS~)Wb>1pmwq̗D~,H&9:_C1Esg;T-o+3ThQ#p!Px3@IE*19~a6p+؀k pגOFAe~ x2wB"ZfcoMtD,IJ0NgjޗPk/L td@=4[ Lđ ?mb[Xp,@ k"\iPl6:YBy;@Q ]C6̾ +hyf2TlԙGP(:N,D&pB-q~`_E8ypm5FcI0 N/ے(RG"S( όV&X`S?ܵuI Iem_&|X.h1𬍯f48e.nN-^t*+^68xd(/V!G: 'f#1ÝO !vT}*.!ea>RMHNJ76)ɇ?woMibO= endstream endobj 921 0 obj << /Length 287 /Filter /FlateDecode >> stream xڍMo0 >Ri5Ic8 *~ (ߟ 'z4\MI fd OHv),|I~:7e[N'v{tyjEUlNŐd:_\45öV1RS58J%VJjɤ|O:($Cz`Y+ 7p<"oyc١`HDY }h=v%.ܞM΢U,Gbv| 3kS+~)w: endstream endobj 907 0 obj << /Type /XObject /Subtype /Image /Width 759 /Height 720 /BitsPerComponent 8 /ColorSpace /DeviceRGB /Length 83381 /Filter/FlateDecode /DecodeParms<> >> 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+cKAF qB ⨿_7RJ9jgvKJKiR!-@+C`]_g&Z4y_\O(EélO-%½F*"= _ϰ%~u236B5B<Ji%rIZ5*E64b/OT5H$2 ƯؐҲ> 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 927 0 obj << /Type /XObject /Subtype /Image /Width 655 /Height 835 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 1084 /Filter /FlateDecode >> stream x D endstream endobj 930 0 obj << /Length 314 /Filter /FlateDecode >> stream xڍn0EY&R3WeEAeUEJG:QSt5/{0g/9d f ;ƠV2 VQ~m8!FfGYŵ y5gg&?r 2K;A >ْ!H$ Ig fYhkB$ [m`xxp2"oyCHp4[3;/ydQ=lj<_aSTiݩu|Y31 aSz w`jd=B q[ Ɖ!݋rJ̈Qۭ endstream endobj 911 0 obj << /Type /XObject /Subtype /Image /Width 1200 /Height 900 /BitsPerComponent 8 /ColorSpace /DeviceRGB /SMask 933 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 933 0 obj << /Type /XObject /Subtype /Image /Width 1200 /Height 900 /BitsPerComponent 8 /ColorSpace /DeviceGray /Length 2118 /Filter /FlateDecode >> stream x1 /N $P; endstream endobj 936 0 obj << /Length 454 /Filter /FlateDecode >> stream xڝSMo0+V#6ct m,8$CM#|fCD(e9X(|AEAӥ? 9rпL5`m~DSP`Rstx-P̊~-Qg*gwѷȼ:\esb`/ &0oQBP|$pH$dJwR<$XxTzgc)8FIFcjێnMw^^JA07O% WZN:v&acML{N)̚`s5y kսqksI4(kpfTBld9 1, =K+cۧo4I fJA5|]h-ȧjtƆѾasr7z o9 endstream endobj 816 0 obj << /Type /ObjStm /N 100 /First 884 /Length 1858 /Filter /FlateDecode >> stream xڽYMo7ϯ1ɁbHV 8b dc ˽6Ό03Z~_X9"]\p-(h] mvF'%.WU*Sfȇq*ZG+#J*Aс T4G) VMd{TY5Ptݬ-tlv1tbR'8* `/tht̺%2*#²; GTv'*tAzH.<4p 3(Bi$Ɂxcr3>< 5Q)|X8N|*aZ'&g=isPO9C"OvIO3]o)v\Pl_!k{5 \2u"kY]xq4^CMr'5:9z=on.6r%>yUc׼pm{؅EvwO=dA7Ż6|$o';fHbWw&ƙP\Es}oEcr:*Cgݾ:jHO|i}8, >NlMhV GE$vmg-z7fO7Vjh K(0sD+H!vOAfu<F>n>mRwt#? ow8 OϠ8]4Xa%ҮӴʯ/g 2NB %Gk}85TľNF9nYp}<Ͻ34^$`{i"J;/Q'AiyrCI'O/9T>d)i |҉ Ws5 endstream endobj 950 0 obj << /Length 990 /Filter /FlateDecode >> stream xڵV]o6} EsH9,3IDF^w%ҝn/&E^s?L a@HE)C)%Xrjp%_!Qh[UH( cn~ F1fHH)hD~MOޜ ̅b=D1P'|u)Nx-F("<7\L'7;Z )sɱd; :cLBcIp7j4f \el`U\p\kg>Nᒶ]Mۦx4lgfDp1MlKX䢱u3spk8ZF26)dR sNNJ';8fK_fo"L|k 4n#%Q3y#EWMVUYGv]D`0q,:kWMF>XJbKOYr7HS|B:{,vL۲e([[yQXW[\6u-9[)T&+΃s qcBdǍQbjI4ĠdfTճ o(! Tq@b;ѵIخT%vˈʡ[,mK;_7]Y,JȔ9%RRRerG+TamA}7n;bŹgyE@  G۬j(ഡg:R'E_Mھ%ٮ¼ex? w !җ?ilCcR<7&t endstream endobj 958 0 obj << /Length 208 /Filter /FlateDecode >> stream xڍ;0{)X֎񣽧DQDI$ܑg@hwv>r2[Xi_!l*BvDYWsS>!}̃$K\\#dz9Re5c9*;n%=`)]&kIjER hƨf܍˾)s9*'F-%&#aM endstream endobj 965 0 obj << /Length 594 /Filter /FlateDecode >> stream xڕTMs0WhZ}ҔҦ igr p65$-%Lx~fdME,eJh(#K_F䩋*DOi",qiI" vS$]i|u3N IԼAѤu>|mPj)fۦ`9.SAXiu=l=)v4cu?jʎHp-nΜ vyq߿/y+"Zwhpk5OX¡q 's[pE` gN+ܗCGVɾeW`EmEZ^eFǼeպj^П<縕\pQR94pN_^' endstream endobj 970 0 obj << /Length 212 /Filter /FlateDecode >> stream xڍ;O0{-m7%o:(1\DA5jv%^\E+= -gp x4N0v`4.:{L}IVF!z@H4 a<O[WI<w_YA0jб*J]U,_%}fQLmOmDy~ )M~ endstream endobj 974 0 obj << /Length 2076 /Filter /FlateDecode >> stream xY_o6ϧPRR))E{ٽm=@XXYI悢߽Cr(K\q1#j83ރG_0hǼ8bF 7[^zsQ{4^$h ⧋'WoKI*M(e$i@d_Տoot0R!I'7ԋI_8cѱuV i@h=B4bͻw#P8?nf8nhx0Л@?̫V"$@]ެxǜRuV~Xy&G}(F-qE.˦(|)ߑU''uvV Iao2߫Z-"_۶YHKMDž,-1ͬrV4"MӬWv7 +Qp*@i0`Ve3ʳԴKR5ҧrof/v 3FR}SYs8gl*UR%>R5د$cm>AX'BduYalKI`5' 且u=Pcvayڝ\yIEH pDBDjzCqD*CnulMĂSK iϊ$Lh1 -TfK;PmuOsCNa#jU^[>Й%|fWSU5y1.M)! f&0MElڰQHXĀ9%<XDqD☷9tcȣwWD)&~ǛAN,v|BAŎ D`%ʅF:Ib02f_G J5P@Y:uD|j%TeV@UZ~}9 %Ljq6Na)aRDAIldQn%@eh Kz{8uuyT(3 :2gJN638rd|:v'Kd(켟+d4TzJ0Lb_6әN(5+EգjHohV; : <(IS_C5g蘅ސR9N8gVQ=]哎y%}ʼn{ ܽaG[lWlAV$M'iV]<w)=e@g4>|[Ǣ'ȩS#̰P9<"EBZGP ]gt Cl-@~#/ g:N,aLdxvf,1(Al{? 9nSA\A5jsHnk~z^UXaIR7_"lvi1O.tmqs*Umxa҄2J) CKYyJSQG]תcZfvx"h>բjQC Jewl'?}zhKnqUrx^Y ^Ȋ OwawbeMK;9#6 [YwKV75ʩy j.*}* 0^zT1AUYfq)3W4#1p/5ea='.@WԋhΟ3>ooZHF+aJu fmFd@q*to^cn̞lgS@i[Mmiwr_~[){'\a=fl3l enҽsLVL$z6-(&tmƶ|;𵻛r\}?u6uw( 'E|)& cֿ|zb)գgU~?V+鎄dٲE*y.:Oa)WR)^T)~ؗdk٥=Dp[ީS*W =pET<c(f;6?55KB.:yN4>~ HۃRu/A8/&)A_FóoSh2鯤" IK㍳Atq|aBiCSxWu@Gu?0W S؏? N endstream endobj 982 0 obj << /Length 3143 /Filter /FlateDecode >> stream xZo/=ъr_nmlA РScGڇ;;/O%m!@K_oSڼ<0dAxsqI&i`ps$k PWL{*+:Hu'/N~;ѰhJ6E}˯jY@ꍍSV7'?(aOٌX&[c"Zr)ͶhJ뱣=<;= pTG۶.+mpq&}3yΝp0uso C9Թ~b-x@{ń|\|"U5LkGwU;݀lsuEKlG&aohf`*͂k7z 0vH b2fǕe΍ NlWs4lؗ=SN*ڷZmP#E nbyWPnڝ _}܃N LUR8JYowU[]olO]Y`ޙPɒ([N`#/+;u.~ v@Q@0tR{0R[*1ms+0͏ZhAB): f_4-"+m?4y:;^&|7A{c9L0i9و@&$}<g!s܏ >WWh  `цb&c% {W0>f7DIhڹIMX"lL8w9&tCq˝!VƎxR;p W1C$ |TY,H+W][smPYB-Z2lW#ÀxsB<}ee/ |9psm7'<:x3}̧}p[FUt9H=!b7:/a+nb_6nl6t瀆ރ 4z|\:&Ro\ekYpQڒ]-cфxhXb/F`LLhZiiU4kZY@Gqm{#=ύ!a%1)$*ھ?\gؤFZl:>쨔y!QDz)+< J{~~u`Ak Ecmu˰*XHmvs{>_MVys#O]}36-pU,IrReXf.)ݨZm ^,Wk?/&|+ ZPx3WmK,Yf1y8 RmzS6bn'S36ADG*~`#49>6[ Xgfƚ&>ڵ B"8\!˃}*rݞe.I&C@I_z9,X.xHh<{#6,s:bA2 2ą;B3w4 81>Zkf;1S‘Di$l[e y_y eI1.8:guصɽ9QjAɵHŵAu>5^O2 eS4C4c#RЛ#.p~&,Ŕt0T~̫J&>~M\{.kq^俀JL4Ab+"al4Qqp,I)[fBo=_!0E73&9 yR;K>Uj.(-[y9O3'mzX;ݦ7 ||'ķs]y; BJz{(p؟V}B# &*rVqk`Dp0uYԮ^,?7יtp{ ^+eƦGFx7S|2J: YszjUe.a9`JSJT.A@h,f{_oV(9 ~od[RrTnWbi25kBvW2˚Ě<)8ms$97=r>[glvuZ'b"tzۢѩĘ6{Ps$KCd(4I + endstream endobj 987 0 obj << /Length 2553 /Filter /FlateDecode >> stream x[~kTT}e8iv{QvpI+0E$u~ޙ$%c8yftޫΆ/U,d  :b:VhU>d~,ƫN4wn2T^ :әs'Ij/ʽ qȻ"xfg8GͫxE`>q6vUJ^=74$i' 7u/up+E{Ἠֶ |4b՜//x/ɻiP߻ 3}+&L:UB.LٖHJ/tF}&MX-%_-. ;.49>p.3Sы<ƫ.oiPNA`UU1ӑluE>ݡgQ%Jp|Iy椄ʙGW0fhsYb ޅZnj4[o >uTZ恶7X h;ED5Cg@\m+}z/Jw-b8q5(\p-FߚLƷK%F./bdˢ/$R7:I0 # :7MqKvnP۱urM%!kDD >C@!-u};) `3Uh3yB3YpC qOqc Vue4I57pYESs )`Y`=b$@˴,P{YQ|QKjzA,ցqVh@Hf[{j ߽{7oK2S_ Ԣ׻PE!\71-"SܔTاeY\60&T, '6"m͊vg^am&4P@>NV~TP@QTҝ2/̪ذ?D4O[Z`Yf+Z{9SExF[%l A봞ӈ/ ,u/, c:Lڗ" 6O-r] E^:Nr즙h2]S኉VpDeTL5~^$a=k 0bya.Hu-eTK8hbZjFр};d;\I4oohИ2}t,yQ.L`wƧ>Ç6>f5YXf+UżKM\\ /~֌)Z9գ- ؄1"hԛ'K( haW\QSvdqV:6[|t9`A 뎰 5+Hrv=/8g[7uϸT]= PC:[B)\Lf+*#&°ِèKev=b m2Q]&3+!4={ 9.dducS~oV&y :Y O'FHt`BvїuAgb.܁`"&^aILP}S4 P5͕fBvpǹ:@=cho7)O**6@^ye:\pVA}BW_TK8gSCa&!:UZ\Bp7p9`GNԐirݛ f\Oꐁw70B `G*۞i9XV)07zKi=N_I9ɼCQM5c }71 @wO|rnu#~S>#Ȅ2ᐒt%@0?p7E .Haʦz!K;ǛqHrVev|bnؼ^dG|ay19W/F'Iju-1.|:9FÒkhx1 /Pl/XÔjuYǩ.Wd~t@"v=Ӌ&>}\dj%|&bbs "(ro^:z `#Ħ:h]` d/@c/Ѐ$bӦS nƢSDJ2rkʵn wMk l< C喭cSUV㖾8 Df` (4BAh}XktuYqOZ":lcoEPG!uʰѼE80 `߯ i9_vq*⇋8.d1b4)9hJJ t^7cyX?llZul0:B@J|:Ӎ̏ÓYP 1?L#dSEa45F)4&'W'5B+ ͳf2??  0F.@ ^t! endstream endobj 992 0 obj << /Length 2257 /Filter /FlateDecode >> stream x[6p23|*ݥOtH@BdɑlfE6-XS873"G__}u}QIet}))OXt^],nc&mx^ܤ)?_wW$9RXEq"XMĥ<+ܲݽ6_M=1ڳY&`'Hnb(Q:eL؝![Sj =܂qˀ_ &$a"J]b%J| _D0ّ GKښT% g#,,|R`x6l6w,(j=P A?aL󶻱G,>!hI5n{_BZ`J zThJ‘> J;eR fEXJЄ旾meo %'LP<*hsO S>$MUGa;rulfCmUY5f=?SdfUw65Y Vn ʂ) %=rED$xHDPh[,U&"9rA+2c0g tctMmBEU5C'WHHXu-O1ڇHL1 ֭ K-gόHS>D`)yVʃ+A)\dcvdw.nOܶ,"dj־e?ƹY1w% %-/i7.(Yi= i6Q8HAsq4gsF(qh=U6*|M]ge1 GXUZt*]ݎpիL -EtA=%ӷtUVWn̳10ZŬ ژY͇vp=GgOl26Fwڜ{ {:d9E4:$Ǎ~ihHuow=74Ū\z :Shts S9RGڼmn 1CBL+Є!g?a׻*G\cmi{1<H;cS6iO[g f}k*;jH9G CYY>j B}ΣdF2%S9JM "(yJ .7]cTl ?B X\7fu3hb'+P;@)̫- eٴeKַ͟B@PpuVU+oKȰ-hk q$z 86Ku?J: [eΏr]m,Ӑ3V[*2w5^Vmz*ր9:fA. 놤Pxh/nvy닑@LodE|ޙ(d!qGɌqB9FyU]*]4i^6.ں ^٠mP#҇ .⬛n 邩> stream x[s6/ nv:;g7I2Z$*v\[<(Hbͤ HbaIakfagwc]L-qK21߶.&EQ%ሺt\")2LC=v&"B_"<|`X$ɐ]k} badu-xFY#V;g#9bqZHr6]"ADυ5by*IU.չ!;ZQiJ| q,eT4[#@PUO\ڞmb91MMQmhEqY-GtkDBZE'8ʼ; Ԫw`A%a1W+oF! 5hA%REfyǴaBRۙ;uڙi;Y7Vɸ4p|MC5ʽ$eB2imX(wb!+\L\voK)ds6/aÃp*]Zgnŕ\)9ĀMT&\cx`io]sljal C"uq#ۑ n`SE3+ov(N5dݦ5d ʭ클QEWZ !ӓ{ l.!eL҉F,T?YDjZ 6RSvt&SP{u]h"yjԭt4[R+* 3}vV]Ezl3bwCdVA:W*(Z(krs)J`/PTi=5ev|tryFg5Spgݣ6nԵr5قq K /vD QT:fS#Τ˅k^T]ݮQ8|nՙ:A?ʚlV0XVupFmB7[ҊYuHOBuo^47D'gd-oTm@vC `97/Uǟv>v!q,⋴ 3DP7YzK0 hpRm 3l=fijxdzq__M/ې> [>ä1cF} 9_";Mgs}OakSGyPZۉ`JG]V\D$oɆ8o2Gh\O(x/jy;ɶ޺|֕?Mg>y7 :|_{t Gqi_io7W?X፸,2a}Nozixwt)_1bdA>gku&7oGduyXE0 pb=r~ԕUΣ )y%Wjt󕦩Wm߉S Vo'Ef}u g:e9@BE +);g endstream endobj 1001 0 obj << /Length 1144 /Filter /FlateDecode >> stream xXɎ8+l I4K3-=Ωmѱ-^>Er[LTz,^U$"dq(‘d-7H1xyen˪?LӿuuR/ڤFW=P0}\^NL( Au+:|$([Dp!)gnByS`&' Q.Y7#0͈‡à dw1bzfֹ$lȴIuG!uleJ\l\T 귞+|EМ8 {~Dt#mu2}Z)i£iej 8 `NYν [)K{3J;vn yEy:>ZFkoeڮ]/oa|W~ Fs1)őn}X]G&-%yEe "=| &/@4_SՕηvNUݗ [NV:{tZAbcym9@ Wu sS)&9>A(p֩:~u)8SÞgcAQd q^.&` V"ʹ E9a4hi$_A@baߓtR Dg%Jq̂ =ɵ jXGЀAFǾɰ bgs ɓzؘY79ߗhmiP xݐaN C#]ɭN3WJ@-E5 A.0! AgHJ0VXӲnt & l$a6~abB!kqc} 3Ր50< ͳ&={~9Heҫ$M珀\gNgH.z\ (?Ӧ(IF)+g] p>% ЎJ^nGձr~`"}ht)?-Ԍ[S.<6x[g`d0s޼^^2LE.̠=Z(0 k !2P,0]̸2ϝnsCߺ#{<4 ǁ!\*] ]o`X;O8>.*L{ʳק-+9L2ndlz8S{y^^}6뾳?ٓS? endstream endobj 1007 0 obj << /Length 818 /Filter /FlateDecode >> stream xW[o0~WHc:M6%K4* NvfH1ЄT*%;A``za`9"Gt,$qu@*OAʪ1gmd -0p 1 }7 #D)2}<Fw̱ -%ʾ@>`Oï26h3=gۦ϶-%[ЮF=̋eO<+jIN}BOU\(!v'kDrZv* 3&X]5#~iF=2n"c+:RWx {5i,(G@Ͷ%T7sw"?TT ,.DOBbŷ ڋr!;.=Aڒ9}> stream xڵWێ6}WQbKi iw&u\&0- DGݿ/_dg_,g ?Gf#:HQ؏l%a,@:3nORxJMYͨ`l}-!NHftBGY6ʪAao>oŞDG0+P'8Bo=" D{y8U _ D8>qx7-N^GM Pi=+$^K傺YV5MؠRBNA{[=7N@#:%{x|/"d/G06XB _Xl IF |J6&A3[vnr޶W) }޼pcTbP]uP+3ՕX*O?fCɕ6a\R@)^EǓDg} JSpP{ .~u78m:e[mhV-o ^f!-ZMn@(0FPiȹC缭%/?|ZGčJj!2UH(jyJXK*LNR|I{LZ}Y_ЩP~_ҕNvࠦ x endstream endobj 1018 0 obj << /Length 209 /Filter /FlateDecode >> stream xڕNA E[&EJ@â(9"JE )}f7`+d\] |sғ!H$LG<vLDOsWܻ[wɅ(TFgXAp@c8> stream xYmo_ٗٝ@M$pAE_ CLwo3G5I<@>s+!gs&:#!Fz+&{g'\>ׄgC$oȳt({>$r/n< Xa!" fƓ$G fQ@0J C\: %YybЁ H8C' BIC@ O8*yE7!p `'X,YLk`bȪ.IWDQMdR"=)pyyDwCOLrK)J~?L="pld%ؤTH$QT~=ScE $a@!4H &aO` #P sbx}3`(UYC54XmN!G&#3ꥂGr&~e _.aEcFЀY\T1 z,}<@ L!RÐt^1EhqMޏG楄d|fPrf[`ljyMbiM4h)~)y}CP ~izi^Ewnk oQ=Vؿ>]sNt]y< d̸Ż3~dh0˾,ZyVW5yuMi}J-uFwM{ULUcS=nWS)yP88|.tzr~ޯP=,͢z^OryMU-ōxqsv6oJg'z~/p|ٻhCSަ&g.)fۓ>0O͋ iXy@b,J-#xQMl߯ IŢx'봴YJe:Xͺg32@ߢ~#j9B(?!Y^Vz?};NfxЗEdQbr(H1ͱE/T1\;D l!c\/붫m=oWUNksutݴn/۶l.g̀/)T]Og:l{> f1EPC ,:؝f6;;i[;LݻQW5+b07xA^uOSdPXw1[DҬN:տ4u|]wӮn6Ճ `o?[w(P=(@*{W,Vq44RvH'juvp,55Fb &:|?ՊG3%&߳12;XB* H#76ZMחE F,B.|<-oq=unUĭqXi{4l Ѩf@&,_8sܑ>agޖ>a>{[< MAS~x~q<` I0Uy쳡 io=:]* }?p.wp@գп~VBUrc/-4n*P2r{B9\ B`#Z/l>&f91>,[Vȣ#fG:6;J|l@Kư^dG;PYr oxyA }|HxQ+O.~Q]SId!THPLex籝woB~o^?wSYNe'} 1["6:MYldzP9?/Yڕf@o*[Ul86$a/_$lC`p9u !6bU3E5ERңeY8-:?%]?۔0>h?c )N3Le?wGe BPMn?>ɭsMEKA ;zJ!6`0o&^>%PС=f}UݿfGnUV`[=۶iuﳮnIa`vR[Ld̕JB gX0:T3H6 M `v{|v{ EЈGϫT' endstream endobj 1022 0 obj << /Length 207 /Filter /FlateDecode >> stream xڍ;O@SY{{w6uC5f^SSZM8ƵzSC/;Z2S<QŗzWU? ֲz(K n:3cH_.|eE&wz,5}=^+?>M} endstream endobj 1027 0 obj << /Length 1277 /Filter /FlateDecode >> stream xڵXo6_/Vs"XWdK4QdYvʒ+I QbV\;}xGAS䠓cǯ=I` z>;h J t_q }~JP b`$Oo]ë02A5*^;:΁?^M6Sp5 $.fVNj>+N%+"3:(fIi+v-)`^MfbP(+A]0ssj,99"}poHQf8/7iAP^ φ 0"?wt峠o^rQZvUD6fYYk$ v͵_IR'` B9XB  `RMV"4{W JGߊ2usVq3xdiʜ1B|v"/Z5, ,=R(Bd)uT`-QqQ|qQk=ׅ`30?U_8gyEf.@aK'eaZʢokۡ߰2j-B9!|T`O)2yI44$H6%e2ϳ2 xdaTJb+ ÁDmU(3q),(h2:%4"@͢| e P`2,_y lf@bPduzvq'j˳᱅w-U ]e|x~Ħ 0Y۬DoûᇏW.,c }_kWt6oâh b^GyԍВ@X!օԄQVܶiͺѨ>-Sq(\/$iu0&E(F9oVxKRas?"uiM+D·װ7nfjq$p~n\= .bk-6> stream xڭW[o6~ԗd9RY钬M+bbUgˋ,K2M<|s(o_(GL{)= tTMp4]zreuXyݰS ]N?o~<X\J>#q[Q(w&'|S|EZvd&\mׂK-hb Jy8Or% s"鷟KʿN(쁩.Jƒ-1^tuF^bcA#V=}&M*/Z-^9NZ#7l򕱱 ezjϙY\e, DEW:tx`U_ۍMg#S% O$оM*vRn_U},waZQ[ZaR1P- 6t5R?lH01$ܲ[m1 T> stream xX[o6~`dR7RP ["]0tA۴FN?~Gd]2m#sxDvK089Y <̨ñ(Oǁċy2ݬdH %͂b` DD:̀l=V8GÀ6]@-~@1JcB3rM<#;>sllQRsWGʣC<'Tqz&H?: ˏ#xdY澏^n*-R=J7ᅪnWaġ6".Y&ԬUol&dH( C *dhg;=? [oGن>e`FWMdAGU^BOmPrz7.֨݅Nj3iqJgX^`>Tv6PP BR;l2]Hd3YyM3s)YvRϒDXX[?P.㜻K /EL&2mS ubue)XsScϹQRc(ruzd*Séвlưk;{]滂EYC]4o4[.S횇,K]aٟi-:﹈6Z6J{dkͬT'1m;k)<~#n"iw՝BWbZՅm׳AY̓-<\KR 96ǡ)WmBbc-$I 9özThؾžQL.yI8 ;X@s-S*R`L$BTH]eX?$>%uTL<TXFyO@iGpMn5x 8F1=oM05qZrj2K2pU%;m^TZ7sy}f]k}E;&lQkeixU8~٘> stream xڭXYs6~ׯ@8HtI%4NG}(Bc^&!˞я2/H#5=Zbwo/B}>z/!˰ho/&^O5~.Yl"K.$h7"<-Lο?Nf݄ h%"|N`ݱѶ\! ɗ Go U/7@fJ -6PjٰtX6b5jL%!]#1l03#;&7˯)#EKEaԱ]ԟ"wX^Z}y~3])"JDOu޾{33`x]rL>ͮf.<^ W>"̮ViYm-4=Od]ZtHvHJW]8$>n/i( US3iY6Ve} 6Dش'wEؘQMQ\"A`%:f0*]{6Ү0Q~3lD ÖM3ѣ…|H"-x GVݍPI/(T x\U$M9r݋1| L2`Z]9|\2qO]D\. t j/L^P/*V1vyp/2 }vHX23p%G)G017 ~ot4*kK7^׍v%Hv< ]t< CCamb_,XJ.,%*|=uZ-:UV"e)$Iw܃d+3LV;dWcSyZtڕr;8)%^g,J%O#U6=*WG-@+kX46J̬y(DZ*vTji>2q Ad=6Hݵ vh\ SȖo3IsMS#%ӥFABAp4}m7"|U^B nYNVw:'{ASۗ ȇcS7KH(PdM jH鵰r(D&C`BzOa_/#IpJmQ3nJۭ[ .EC6{TJe:L9w>!%A /ƩIDLRlWs|Z!.w% B1; Foe6ajo/]( tj?%Ө,jv+#>액a8[%;RIyeƥCmVA01!z3]T xyåcWR}apP endstream endobj 1044 0 obj << /Length 1240 /Filter /FlateDecode >> stream xڵXmo8_4҆@0H=R6oN{7aK鋴?7ܝy<0 ]E5)#. fv+;YzC!u,,Hi6b> D$Zs <2ˆpDdE`dI5 Z M7q[M^(g3ߌn'%k̬yL097kDLb ]S%(f#12)]L&"&M=' șС)|վn٭zC:ac]sviˀ4r8Y1wxp| C  [n7~`ezn[rmu-LVZD/^P"q"4CL P7hmYhF0\" \eDš1+U`rAl)^w)D\8D Wfd]lfQ\3 }&/4gg%8 ʘ.e-PG1N3 |lfɳ?aoQfɧc挺,ql 7Hc G'$+hѤ C1xd$P?)N;q;okybyq?V7U&rg_+P0:tu<>wb5I){Z&,*b7w%$j4+bq9*ϛ}UӦ4ތ%ieC*Cĕ#) zZxm*Y#,ÒdO,Dwxp#)ZC\|FZNmO+[R6sN;"a J1uV|N/,-KN2T8ae VH -볉C#b!;6С Gh!1*|(ФU\l(™QS#㬦t|Dg0AB~ endstream endobj 1048 0 obj << /Length 1130 /Filter /FlateDecode >> stream xڥWn8}W0K )˺)Mm>`$FT,;$eDze+nIcq8 a;3<۴?E5,oC VivP~(g碅Z vo=\ OS@_Z%/mD:,q)9.a M$ d[2I922.Td`c|,J?tKV4̒L*/c-k`_\ ڟ(Tk͵_xwyr|@xnb߶=R?_|J|BK1b*_љ_]~۩Ebׁ2Me"I[+-iVX?0j:^s, E Ve9Ke7,I0/{~⌳*lFM/ّ,9,9C@.J0daR1۠@0|nm8.Y"oIƣ^_8ጶJB̛'4UnAVk3\ ^k&Qy{r-V֐6\ue佥+A[usVF+m{i1m[[fwu%nWtV۞ѢY56.}3#ЬH4:/,8nՂFbά*Ƃ"E*OqƂƱ6nԲ2˂nN"i:1oR*u7MuiŒu`ZgI1bٮB3}tvMYx5IEuߤܰ;d{=BguCTlEcYizl-rvu5Z#ᒃ>i.XQC4utf"llr endstream endobj 1052 0 obj << /Length 1208 /Filter /FlateDecode >> stream xXn6}W0K\Rwi]d}( hG,9$||GlKMF ;`D {(u}( +Ѳ5C3A\gЙ@n``oX Ŷg#׶ 345z\|k ,!m s^ru9- ۻUnP'PB-B}@AR:c5=խEd2FK}{RHx~7IaMZ-| Hy'aT<'j'v] ^mpxsbNXfc&_L i5IZȚMɚM51H!~] Wtà ZWa& To0-.65CVO]y}btÆOJ7~Mw f1 *'iϝ:'kX(ND,ڱ.$ 8*(KɆCO_ݢ> stream xڭW[o6~dYRwÀЦSWDZt)YR,۴+5D@M^n(ġgy(#B`'QOQU˩e\缐Le޻g(1̢zBATtO|O J`-"rx<3qaBZxDҋ7`^wPy6y- |!}< sZQE/?ngfDS WzqW,,O)v&cD#fQpWV>SnϛٵfYV@ՙI8^١?ҙQ\LbOYɒqU%Ĭ%xyDmO)S˺\qQ&9*nZ׼JLs,7(^NZgHͥ|a+_РJ216W,K72fٽ,Wi<0KJa]VCFby%K1u6&52늋|IUc)LmeDI:Ocu%nI) .8ƮQ;2{U{Wf/To&7,O#/T1TV\&OņpRہK)>TU"/+2>'Ң_>~Ԟuq Xym.2g,VL.w{+6\hu̧&$1˲/Ҳɶ^7 |&:c Qj[MKM*h.HNHxڎ5ƋEaw?TV4̆|"5}6翞8&\اA"ã6 pSrԆi1sgsly0x"X%1$]8.&=zVVPu!I<z;_ endstream endobj 1060 0 obj << /Length 813 /Filter /FlateDecode >> stream xX]o0}WxK-m-Jtmq6-!g0IpBiXiڦ<ع #~M B ` 8qhjtb6QA>7 D Ps\Z ,-d_ 0Cn3p21L;P#KF@N"f9, ȑxf6,! $ zTŀJQ#CBM* }GW^:nuξjc&Qd 7&np&gM|l鳗Y(?rsTD9L})ƽ໯]{V ̳v9;,hcV0.e tD?`6&^4L}}$*CCKBP0T<~=5E#~_NJ-RT}ta(TUB+dd[xjDQaOT0i,Jx(=G縐je_Z'e*_D+䅅U9!L7ـK Hm|:Z5]Nbłq+BEi6;~]kwCi35"3.`~hԘ>bVIH)_OZ}ѡOQ@6wF)o6kJ/a$w~ل??҃1_7[ 2ȲNv=uE(;eS1cϊ!ҠX6E:Dlx=ضX6rTrӀn܏W`  (tΣsx endstream endobj 1065 0 obj << /Length 384 /Filter /FlateDecode >> stream xڥMO@x)gw/&H (f8(h‡Bi Fcz;;K0vtFWC`t NuP9NWHbe|.UM*RvVGSJR#Y'q *x(w@[Lj6Qo9jkfq2ۀ$xE"Xf0: RPhDGi^K'zZ%NY0aDLM3uTa߱>ǜ *;  eX w|QaƮFiݍ'(rieJ$R|6^oy< 쩔q^D (n+d?ė endstream endobj 1069 0 obj << /Length 285 /Filter /FlateDecode >> stream xڕQMOA ϯq0e쑏E!r0h"$A 詓{`JS}STW!Aovj4`P$R]_aUghJ &OEѿjV.'nl5[#~x{?+&͐rfuykSYtGd0WRsLEw28>r)nF'л5KBqN̾5 )~)_Mssw!^&d% 3t#R>V~ endstream endobj 1073 0 obj << /Length 213 /Filter /FlateDecode >> stream xڍN1 E|Db H"̄e,0 d@}m]C;v0Xe!SG4<·r_Ô˒ݾ519͹ @6K[oP?Wk=0:;_^՟F#:-5-bPtV)~_]禦2fIBr.hOy endstream endobj 1135 0 obj << /Length 886 /Filter /FlateDecode >> stream xڵYKs0Wh4D!4qLq3 W& p1,/~+C\`y6Nr0A`8 hAb0_}99]?o>hUh,h4ng5V,8Hy"-ؘ 1r,i MbK&&{i `el(ޤtY_40<3閾ºV Nq!wO>V.5ÝyDz(y'boY`Sm=\Nl8viGSlQ gԸiGwӺi m,wnl.*6>$I?j_sOv&컥6}-enJL> stream xZKo]Wplx9/0$1hEE@ ox#Y:Fwp߼CBO|H Mb.QD$"H+b_Qz3=ђ1GcOh v$Z!su;+8i 5,n60,ѓ{O KnsDO, z0a1k{aIg)c4fӣ"7^s2v)` f#Mya^&Gs(8 , * 6IqQQm7)R 6Oxdj bhPSln}z0=Bot/T4O@K"6LO MLJ@Iq$Pކ ԓN(> dD#)*D\\=0J.et ɏo aJ~-4B%P#8/idј'l$pmsmL%)L`P9{^?廋wo?#蟟_zc~?|{[9:Wg`Og\s圎Iy9 s|/gؾߢ=B~73vgpFQhHgB3?#n%'=T:7=mnsR Qˉb0 ;dj5t4\&/Q?%z-j endstream endobj 1169 0 obj << /Length 656 /Filter /FlateDecode >> stream xڽK0| p#T'n}ͣ&m rߌg @g|5pA;`jL Oۨ΅TJL>0MLRQn-#@=z'뫅޻;| }"w.g5!ػk5Nz.(XSnZՏ)$.e̐3 С.ib lJvԧfAcz)*̋L m-('TYEaH jmhe:p{eoyQ&yFUՊYrnjlMò2䰑"zI a, | 4pTYEb:PA+rr&E T+arqDJih&@U2/I9<|uamD)SQG63\Z~LZ" #,ݪNKm@omtmXûTwiv9PT6U/ӟ)t wKz&qWIcvߊP%ϱeQHܓR1&]~[6cyF\ԮDXu endstream endobj 1184 0 obj << /Length1 1396 /Length2 5927 /Length3 0 /Length 6885 /Filter /FlateDecode >> stream xڍTTk;UH 20 14ҝ4ҒHJt 1߻ֽk{?{{y>V&}#^0 !x% ( C.?nVS'o%)(rD%$@ (/ C :|M8 Iw::!Pa Ppx@A0qEhr?JpH;!n>>>| WO>,'pB+dowu0GSC"x ' G僼AP s@UB gJf8=~YXʂ8.0?@e9 'K? Q>B>)C?\M5}>Ju[@{]Ew䋥 ~缌ێvӷitӤhf =hqd=Fu]i>lT4#PS\Nt!ŮxC5{פxLlf-w>d+} qO4C7G"U锅qM HJةݕT,sf tOOܻ_4Qwk>OHNb5][+0^646~%,ExEǤWy+%ĦP=A$geBEՅgc3#c' ;[<+&\dȤ & >-E~zAqE*pLJ)Po5+`q)GqBZ\t4JW,l vxS \ᕄ!fpFdؘ,#- O+j Y=]-] aS$䋉Ѳ\OCҡSKuz/@5-)y[TXld#l=TFj0NOR E[P>};Px@-tERֻV5&o`t+戸д)4+*푬H;l9,ixbNb$ 'љImak'ss%RgkH?ʫbYq"=o)k\pj,*Оd#~%䇥MJi͎S=|gDecfFQ>΄1O sW0xO+vۭhsk|%;gTȵQ8jѷєR~7+!cs10''1''VMi뼫 W/bh>PqMKG06WPVƴӕ" /ݥZ,}b>#R~-S2c7زvagL!o.vl‰4ЯwmQD^1b4-uK_4pڝO3ݱr)5a DYJvS6EPIt?BsEUbP8uή6yx:o𢻤avd˪d!s[rR5_Qql6{&>A3o|y)rl!lVU>xaY;/Q %Ξ<;, {ȓ4r@ y~Y}r=¾Һ9ƣ3|6-Mxk-u -ŏ.1քQ7 hOZKu5esBF1" kQ6-TϻzWDi85L2؉Q,&1jxhgk@f/p͂&=rRdpBXw F“ ],N)gw3ؑvt ߆<IJov[dc{o\F14z6<ݴm|bT|{.P]< &,ӻEߠn+9{fmx!j2zKnyԨ.'7Oq7,nF4D=4r(%KՃ T7;b@ח3YZoq)v/); E7Ibz u\.jEJbhv 0yLx-p8=}/֙*QNഭrN)0n2wOV6 Sh.\w<}RF,6w6^;C $I z,- ;/3M4OD{'j@߆3J"n= 샮d= o |O-EW2LVC)ձŵbؑkl 'bB/ykk- 1&iQd#,ܓZ&uXjygFs;MT<b^vkNɮ>ݔy N?lӞep3V k{AFJu0MXRn2i}rEGnE$9ƥEHa{dwf$aAsӕgv*jǑ7Heh64з7Fg #IaL~Ow4N?@pU? vv9?\f S`y4,Qz" \Mfh~mZC,(7HϮ| N ʊR(T ?C/-b]̡CT. sV2sMeWݳxSդ)^mo 3CU{,Ij=tѮgtP~H-uBc79/g'L;sol(B>2`xI|:#L30NYiueEyf[}1wX9<49XŨZp49sתsZ>ǿǦH ~ޖtf܎O},vD2lqg!֫s3(RqǔC:^,wKCpf 6Fr_ZW1ff&g(HogxUp;/ L_.̑7ehsRy 2C#܎Xtfǖ)k'.#ñFoeߦpQ&Ei`uXfi!$}M/ўV~{v8_~PfZ꩸(tzU^lڶG<_dflPC6\c5qPv5kF>M ³ۮHoœF}AZL$Cc ͱ6J/v)S"H/ ޻3\h\|ÀtE:FB,.'4f빖 LIR5T ~aswKVREM':?#ach'>sI< ދcplB$EoѪ$ӠJhKOJkppn>LI^}dת-*y[zgw>XUI%|L@!`KX4)"@<)\۰]rrM1_.Ml]Ww1LtGg~̍(]|W-lPᙴ4 <;n_܏~\vTo0Kf@TlU(˼l+ S"|CC~'/`D<*O3ilm[,Qu {2 .rJ_xK3䷖޽s@Zx!|lSznI4;Osܡ5'ו56 Qֿ4zvjxޓ)M;2\/Ob#"M˯Ͳ# 52SXZN\гbkz!N.7:alˬZ~[,]A8cÇ|_̴>O?VBƉ6wT&g`&(5ŀ/TI B747 tila%gfg9kSo;cvdeH˩gdžr\6)DHP,ZM;ք= pd 2G@Q70QǦ5a۳"[<ʮCzutpqWZ^hWBSkKJ`><4tާbpc։|¼Bqf i5 D"*DHbgC#["/Xj~J ('RT}*GWٵB3|Ku]L^3sjTOhJ .ui~*{Hf$n:^~HDTy"ӧE/)m$\mX<$JT<.XF K7Z o ^^Vҳe_f^4FG 0E}2삗 ɍٯy1vDnx#ŘOh\IONy׶) \.;0ico.ܴl׭m[>12'fZ;dԟ-.{9rsTrV => K|a؁I3g=ϛww X>*^6P^9_ň _ό!(alAnqy&MZS0o}Va dPSR, R.1k<+CgbUH tPn` yڊL+2Kfqj `{"xQ[x|ߧ=ق1=@䢆H4$!Ar͟ҁI*޷8 4qB& T-؇PHDnZ92|(14PD)G:fAB BN=٠ph}yiLE]bWlr ڭg endstream endobj 1186 0 obj << /Length1 1376 /Length2 5980 /Length3 0 /Length 6926 /Filter /FlateDecode >> stream xڍvTT6Hw$CAf!fE%DBZAJ:T@i~Z߷f3sЁ 4 # HPc1C ¤H⯙D@ `mJ0 FN $.-$! Կh7i@ 扄`@Br+]|ܐv6xm!)) p@ᆴM@6H)xe1iAA///0v{@pD_ Z0gğ܀=xAܱ(8 n@.?l!п A;P>H`tB*o ࿀0'w46 C:ߕy]mo{6nH;E_i+(;n# E඿{>@!]=jJ!Xlv ^Wz}o/3? `mE`HaߎH8X#(dǚ!S{BwXz('WPHCzO)(?a!@@JT$fс!VO H){J*yjg.-4?7AloW 'n sF:`9_U꿡?DU:Ga, $ #Ucc1~) Bݑ-(|Xy8bXZv!(4̄̇e'#Ȁ `Cl-ڍXEDAlϰ>ps7 k[7†4vC]xI<0r{Tqog&>Q긫4ǦYxue,(( <{sqfHZFeeoz3p"B[djv (:He\cS͑pLht L6̵|{=Vo)l}`!͓ӡpuWg*Cz]Vڟ*wLz# eqsNH؍<zj)ۦMu&i6$+Kf@gLu- kSwg+︬-$=7lHL*ϗ:.y:#WzY-y.tH*`k/Z)4tijJ~Jffۦ>*'6ڦ>CI@o? UW,+ 4||$e\0="p']u#+~ѐj} ;aLc}0R<Jo{}E@'$ ۙqө^JCj`=߾bJ|a}IUoɡG_#Cp\F*ٮQݞC2_jM]nf&1zF} f0Q<R?*VMr _XnXT%E?&CnԚܫV ݯJ,kz;\+3ҁ&q䄹cG˩cS]3o|G&*vءɢ-! (8zD.R/Po%KsT>? fޱnVڏF^ȅ[ѯ GYI߯'QQp2< A˦OEl|sbe =ҡNLmI`еqIHPT3<'=sթ\1{տ [5|g"Y@š:{l[ؿƥ!"}İp|[RĪN%LI6DxBh-*2bQӍ~i*xepL(fE/PoxH|6e{!{"z*=;!vC1PZUOז޾Mjr\3cym/!%Kq ZxPHP=x.Q 5y$l\56:6t_hY9nϥ"8rjl>Seh||q2f Yr?T?ϊO*P,7j >f4S i!>嶙=hTQZBH J0*s&awfyhy h~OA-Hw djx~v9\v7]?[njIΊj̑, юYLMD.kǦIMp3fU)浏`%2U4(8Et70Po}wJiWr bQ%?DP-*>R&W)]8gBF,e#XL<|` i>ٚ~%ܡ"ȋ,;>,Yld Kh-T(3aZ6̣6CoJw/ZhHhhs+Kmi۰CF&ԾywʅTwSv$hֈ"bQ.Ǔònae!{y>tRl̮Pz}6iHtI1 $.ˇ鉭Sֹ&⹈{Ծ_B4dĹoW;_-ɜYT 'vQE<)48H?&yJފM\GM zt? <]wѰ?#s[i?be6}27]NC\W*JS>偺j|~zNC֪b&2ʌ BsQ tϛn &D=TK'u]ƹv)% 60uL~}s^Ib>y"\ݶ~4fy y,H=+H#I(yj~|J_}8 k=mξȮ}jc'G2KE[-6ĶWvm~VxiX2k{}4ũZՠ]v²6 tkR8a:w'y#UOpƲ9?+H/AΪ Xy)=σ/7F]CI=49PӴǵ^|ǥ* [n-%=tY*c3 ~%FhNOTغMjRzWHs&3&جo4_Ofv!p­ "WB>_E-sYMsRqTAm^1nn :ѩH6,ґs-ƇNJJ{hAEroŶ:(Q&k/J|9m3wY԰)KO rNde6Dl:gGh4RZ+.( +.\'PQ=kOc_Xk(ޙ{`B&;k&}^rCb9`ϲ9@~+Wd7/E![u*&/DWE }Ï T"{3657Z @k+\r^H &44 Mx?P OԴ6Dt{a޴SĉsjԮ5JJdu-eVk殟wϑMw#V$+![;M6H.tϐ@&'d^ajʣڶΟDɌ{ZR. /wB|@bO4~hJgoi&oHۜ0Q}'~6jQv4rxWav+Zs=92/Ѕ8zW~չ 2vqZIż7^wqH~FJ&%g96^[,]Ea|ڦTj`Qxo5p,sn(:Tl9.'üE#eI% #ٲY( `EF`ًU.$~.Xحs ٴZ20TiCqfn~ZV)#=HܸG>\&y`.Koeƕ+$c|5Jo'0M\.gLRK1t1"GcVKZeMi2g,0ZcӺg__{*_npRhi=kFy H.V2 DOWAFȵ+]53Lu Q{f0fGH^{~+z9cK B> stream xڭc%\˶m].u].[]m]9gfY_3k+#"w+ITL L<+;7y{9z[3_#;93^Ā37779@@IMKK_B&^w=;hz*p̭lQE%miI:@ht6(ZL.@j3 ?0v\V=M@g;++:Mm!n/B#)9:[9fU7OWKcrXuF9Sҿ|az]]@OrfV.^sst 7+{b@pZ;]\;U'_8XmY4uA7w03n?}@5ꟙK `4cTpp@2 "v_BK*_07@5?Dg& O Ү loW &\$3D#tc`z U%ы|y\~+#t~oAԾ= İD 4aŲ@H9VǴiW6ti #|G&jG Lè:`kt6 RjȔ+d&5J;&'-bw=fVnӺTbg]yj+FnPG-a5%LT9 KX q>̧L7qq/= ШY~Pq3)H̾.cbūF۟4d!L4/j6&]"v᷶DĖY1\w(ޢ[z̊{Bs\L64i5r(+Qhڏp5w?%;Tf[zF:pi~lDp*5Dl F;s[aR .̛p*Ģg߁w:iSәEx2%3F6mV0D#v+H&7Զ5R(F?:7+u~?*vGGH!R=Q ӎ,/ҙ6}ҥː6O2(?#:2AJmڔt*16]6 y9?>I^ay9njv`a`OWl|:3TƾODbUV23-]^2Ėͧ [ +~&2Aty(Dq7aMqI n`GB9~qcut>ğia"U-"!A4lLG*WҥGk2k]Ծq;r*֟}WUطa B 뵚Q[ 66}6)n`QR Zo?6rtj93^՚"MmVl;ݟI|*ІJ<3u fd&0T?З xs2NB>VR-S2Y^t$̽.;|'Q]mϰK&f ,a!|"էng Qc/ad٩'bJcRk ]r;j%~ }\یP$M@nXs"eTpy.-SߖO\.7M/ki5_r<*7gSZْ\t| YsMKGHRADZg9|S,ﱁ!#OhѩsBo cZ]%=I!"8L|Fβz,I=؇$#[}=fYbԝۅs&LNz@?b* /9֠cU|YWVM&siGccSh=qWvEo:'H3̺ѥrnxf oe,Tt'(lyx09K?;N[kض褲vh,ΫjL d'2DFp+rhBc|,4U;UРm #-N!aĔ6ؗ)7Z\;Z*匓`py.8qw696 +&*|Hx.fD<9io?j/a)CJ= زPW" cCeM]D X$!'lb "}i5׉}Ƌ2P\G&[@ gqJ8kbu?=F @R6鸤+NP췥ƳMMCA]ue&41fZ_ xsDT!\ӈDO_n\> Rp!."y>lY(c_+.EpaJnM&ex R|Kҽ9E%DHٝ7m !^7p"e_'[Vm+ &ƙfsC9)V)zo7ܭyBt.fYkrgLf!Z:' 9i vf C[ q<bNO7_̴Y:ѓ}vӻe+6X/jL3T-INd{U~qc|*tU^ԅd:F6'G,'?eokT w=:qQ[  W,%2Ӌ7bIc̼h#7"࢓NhutNC9)\3t>pMZ܍;Sxeu5GjDyV'1f|*StỊ?[ikñEWƘaO` T؊CdG)`|lOoIWsR kƻ*"JULǣYA5Ȟhyܫ;@> %(ջ6EAI]rHMeC'|W Q酥:b?,{NտҐ0D4R,}ǐq'<8!1gu ^""ݾJpwCށE6hΓki)q<2: c@^^6Ml=Sd6?RhLhvoWQ5>l&דJ]Re ^vZVIe]bqX&˾ y<g`X8i^2{ry(o%S5Ghr>F~p".ŗtBWfTL?Ңp]Z߷E+ړb?C=dd=!0$\Ul)L{9ee_H19Aqyy,E9"I[7s+3Ӭ}Y{oHsil,] ݾPI'KGnٟծ~×Tvrtnㅖ ߋ}6Ҿif~z&kחcaZFZJe"A.gPҘeZ#1ų|D {ڱѲNĸ:ã~z:>~{t'iJx,S{(=tFH6ge&Q 8l`)hPD`Ǯ2}%C63= nVmA40sgO68jr}^ibȥOk x+D ۥ#J؂2G~<9wX. >Hbw~0' j~v d47uFfi!?ynPF?SɊepWFcjϟ|))W p69C@-O#fB {&(]prhI+9iG]Bץ?b4,S3D>J!(WwWbjimptVӬ(Vܔđة\M$Cvi[qQ-Į3 ٱR{VdEOy r79dIJPb Fŝ=~IY/Ւux?F^m"jPE*>\s܆iĵ`]0}O޼+Iv׉*&5TCyN?%Hq)Ğh;tK0qӏ~ekIC7UE+,2'#{w3;>dB(DT?|,ꑩ\o>4ƜSFC~G3;L)V`S:{Ğ-G;Ȋ'ewneEu\W)H3PQJ jĐsv9=tIY;. 4^:kq:/ mvNMO&K k1{ {#OGT"q1M<ߵTI˹6$~Hd-=/i;j4+=3 _IZL^ڧcg\+-s+4PW0HHTϘYNw&MÚ(_niN/;03| YE7q91R H(5L:ĮQV1 {ُX+E1UjNi溺K!$|Rt:Ꙧ@Q֍mUIir3Y; aKV ͞_=#ˁuoYC̼v ~LzoƑ:R ʍo%o=e4tՁsatJ޵`8#L[v}!0uYt("8ɞSK$fHz7 <ػw1hyH4B[wEN2 ,R[a߲N;PFoJ:ƏnPZ,v8"A#G/e"w3}`Y}lRL?JcEyfz _ayE"6xҿ ?t##qskwJd DNcD$<瀹D.W3yu["*Lb'3t~ =]F~#h*1FuҝJs'ϐC=&vccxx!eH\YJwʻK݇1+"̑S`L G1 jEAv-°{A(VRJ&2)2ՙ됫G8t8q1[CAA]{"jг_#^ZH1hIP öPFE~ ,iۤO{'fe̻/k!_kk1=~3hK 1 TtV >Fsn.ʒ \OYtڝ/&l*܍L~>E `]/)rٜwo)D$eCV>J @1N{X1~] UH?J^WL;iy]ֲ$^&՚ZRwކ 5-;S3 J}&~%Y%4= kWM~t^/wfSֱQp{W5@SJ+(e#fduTgM]o,}oqjMݶ^tDg ĤYz%7tQlO,JF0U:}90iI;ͅ#%ڢ x` QT(U].DOlWJWвe\,Tpч˪Y#nƹ$S),Tނ,/5S#=Ǎ5tmL?KFX]&2a^TpRnIS CPҪDKD bT9ml@ٓY6YP^B58!S14оFxeSۡWJr`-Ji_8T ZkY"%椕FYgQFlۛZ?r`򵱇d|=ޯ}c1DZڊCɃЙGj CGRy=[m6`J9j[KFwҺ$J|9?&Ztp.G¡_kOR)Zh ?7{.HÀ 7*Ũ8lj)0,yQ*7XT=l`|*8?Vy잜rZ|"3p ;AxtϽ]neX&2mpdߗݒV)Bg%atP'8`BMj]iO5~5Orͭ+]si0O^VXPZ3,M{i(L3{6k ykU™O: d_WW}pF)hG3J_eA2Ҏt9ՍlןRʩz%*;$cLUjt_`P|#᷹b^:]61Gɓnͣȟ2By6Iן R02ъf<<7$NqDFmkv}\"NПԳu(Z`wslo}T/!w+ ;~m}K̺IdDK5F#Mzł1#PU 2S3Ɵ(XeҡRqIZa1'3'89Ԇƥꢵ묍[@)Ĵ'eŒvKdohƆp0vՋnOPhOtQsOÚĴ)% [m!aZf?IUW5TTB_+d}K&fVxO屙BT}[C8,lKB<<,̡riFL=m|C]U΂iÞq mQdϕmt~HQ;P& ;B%'0'`!,;2q5各 'uy^o^kQ",zd+h} )۸F(kXbSAMirv $ƙ0C<@ˆOHYy'~ ]7$.6!}9](j:>9:f;5>U4DH(R~'5͘NjiUG|Pp/sT\ G,(菚K 4zؾCaI,+U<\n8^s l 9s4 !u劑BU҃ߕL@Gaz"Ӂ|R\V!eDLknSZYr&[kg0rLj,~y\Uۚ.  oKrW3֥K׃4c #$jG*. ٧ 򻻝~ڥ%P瘥78*~\~3(<"jMb3[w& URQ3Mڇl,zuzF]>-sjkBj IzyK<$8 rB`b巪=b3g;)"d4p%lQnc=(?'(8k'K꧰h0_tDfKYυRWܲP== %4ɒ 08XȽ:E})dUіQ_A^&dbu'|3!^X^o;r:lcY6ﳿliC0pA:^W2Rˢ.E.xkGV?xL4|2эnJ7DPuήq%R/'%zbEpp[`fǸ32=VAuSWa!Uܹo w/s) |BK1MGBltCV׏tI1k?eOQ(͹kIR7v&yꅋuzt^OY WNr(0-bf@&QѬܩǣ+̶yڪO8z.OԤʚG5ܺ1f :9|qR1\5sJyRw11;j?eDH$q9""h ?R\1f3enio|4𷙾E mvUWZ bJ!$ѽScim!M6kJL΄Q\G1jٻ[8T|?EPqd@wQbzO  *[L%I%+H'}u6cEqѾix]0B Py׸mHY\3~y+@#!:*Īr7"W*g7wq3l?N,~ G4]iOM\}&vJr/(ra܎: r^lUŨ' L7{^Ĝ̂gP_#9meI2 $=(Wo⬺GX!ږ{).YQyI)ȞR(xq,/mIH \|^Pa@pXtgؕt$PgՊW8D΋-9R UEUnK BE'[c,pBQs5L jjN~ٿX9IjYuMGKr,0$|M=JFDY`a/5zgۯ ]R)K+8AkGXj?J*2Shēs>$c8p0"R9/\YUG,_Qy bKM[,BH*JRy>oNGT ~i?Q.~N/ɨ@`"]gjH@źHrU8W"io٢?XcуSwz~ o-yN)x I[W-z;cslTlnfp0p]zMbgH ~2kac6繓j.{]>ZX̂Y4*KopLjL{NsDXP4oQwAꏧ/RtMl<3n!觵Ky K?{QtRtc=T%c][4:*fM)IuP<`lX5 οգ!٪Am邾9Z|%K-V5.L^(K5!1, vz;_.Ġ&zHvmpLj9\A(qp!MpJqFըN晳d`-"024rcf#8#^ph1 \SDb ={ "QHn3CsCf;u[Pa]/V' / xOlǸΝcFma { bÆaRT6Lӎ6_%4Uٓp.{[^+Bㆡ@kԨ$@m _,Jm6.@K}=2sbei;p &<2Qd'х1 /Hd…]܊ĹhyhlXp'yid2A)tL5& #Df}HsCcMȾpո=QͅuusQ]D|V)i0;Oٓ\Z$BgXXZrcv endstream endobj 1190 0 obj << /Length1 1612 /Length2 19391 /Length3 0 /Length 20234 /Filter /FlateDecode >> stream xڬep\Ͳ%*Ybb3bVbffF-fff̝;q߼?3GGtef\+zhJRy%Ac[C knmFAhkOI)4ڈ\51@h`eprrSmM@E5Z::0tߝ6_Vv@_J@ d[rqY8``w227Șm[տ#[cJsd%08nq掎6=m!nb/Bv#:@YEdf'_7o?%d`n]A2 f`/N6ɀ45p0::OwNVۿv+q09LYX4mjnϠHژXm7v3_ gfh00rMdmAShTfA[j_Ŝd / 玱2pX[6@5IH 6C ̌6;AFf]`en迚 ``af/>e3s#KZ.%WQgԐ)H_Ef,姭Z!$d `{X&`a?d ]ZKffW+#jcdkϬ( l26rrpN߂cA]F F܁)驠q=,AvyU>)[oA \nN`[Qw'/r i{֩tRO"=.e6!5٘Uuޠ&ۿ9^>;bR==k#X{:l`©)I)ɟ>MzBҴl޳'Yz c|3gpV",6wpr]) /a*԰ +M:Ay$ȞjDcDMJs y5A}!˃1%h~SE*{"XysN;x^J=1G'6''qf_a#;P1!jl=$C7.B 1o<@c א̈́+2RǿMSԱY+R-m;}wh2TA2j5冇] @t[i*^FX[%tg'2:=É=*OZД7s/9LXŏϱ]Wo (aq?}AEw7JSRr,GF/ {e,DJ.uw!\ڞg-8JF]6YWYE1&vCjރ-%nHd kVo&C9iڸصv0\HӃh·#JU/ۡɣM0"V1hEu"|}x\m'czc.ԲvqY7Z(bw/e7y>2 sB w y ¬`;hn*gt!1╅ V|Aga1ۜ1XJ!E^STȜ=AwKo<4Ry?b6:waT ;/UX6\?n(n~:IWjl3rx[ΫQ (4?G`G ,avi=ҤzlRdK31/i?GޒӜB+Keiagxξ2፾VJ|~O C k`%[J ̈\#T@1I2r!^yMgx='Ώ+JGaju3(zμ&#_^]xGZ,'v2|ɯP]cJݽ,D]^*#"ˏYZS}5OV#V |LeiyHlK }Pzl Cy)Ҷ{DZ")+|V֧ђT'+5˓,zh` Q0|!4\޿H kD&"bCf10p ( f43K=˓_rjJ78)K3 \a.r#Kx!^>9_ݕ_ 7E ݷzu-keNMmmQ? v>\MK{ob6Ϥ^~6%OV}v:|Upa"bj+AeDySW%BHA>kp6Z6iߤom.Lxje(JI_݅",j~+2YþSkAPah8uy,yT'Q(C=~˄k6r]_~`~巔d(dA؁kcSa9 hǪLc|ʶ n >Ns1JHRtt]D yu2lre EV*CZ&-J0opߦY߾|yJKkHe*1PDfL}kbP˗hK9[K I ɭʴdb1J cg\"q~RN\6EL``Ycy5e-$v\ d]%{1@j\oĻEJ4f K|P={&OO Tmq.#çbIpjH6`'@c>TY]ݟ=) rSt$w%#eiKKyQ~5$k$zxϔ:y߁z0:Vđ+@OKu! ( dS}``z׭43Pk 9.oÑ[s}e;c™l!t+R,v},iGHt5yڍX¯𗔬6D,A^ʞtIaуczR>yJ* /cgx ")A !LR O}AEt]b =pREĺ_n JO߽*JՄ$Pz5n [J*|(i Ľ䉋_ g8iI$@4g$9+"ަ!GL/Lohc·pٴ^TNOhQv^ly{EN2QK'#e zf3\G>GDL042#*M^i~#TT0lϏ[MalY$(P]'j~Kޣؘ'Lzv;V@1zl}\+US.16^Y&WM$KjD$:N@IO;!ug2N^,ä:}[m-AJ+d+?:G3:>G*8Is*U7qϬ֣B%skk˗tnk9hnU㾾Cv0mY[d(|2 =5[߰o4{:aww Y=UjK vo)텚K~At\vQL|u<eѓm0KC"=A#%&OGĥO KV.HI"۟WGc8wmgě?C,TNFbm +hTs?Nj%n*%5]pƚoCw~zX0Y&iC^[g<&[i1y~ gyUl+e_Foi~SB@*]DU%2uCDž4QN=$o:L9#,/Xr꼼\wK<ª>m޶cqPgp7 \aG҄&91)Af}BH6wg;#<>c~PpaO &},vlH1qc5T& dڂ_`~I-˒kU_Fʵڢu̠8.*x0ʑ5bm]LA")9+ɫvK$XQosRiL#QTAT+[ze d?]!Q'JWL9VlΝk{miD3ѐQ7H0iDmyQf>|춙!;=؁a~Z2i ϘDcN)0eBֵ1ICewXF#Xi̱D`bjGs4y5wiK`!6P@SŊj ]!;RBw.Qj6&N P][O|W,HrIzGBV\Ͷ,XsI9C< 犤b$ǣӑ 8޿Ùۊl+^T˥䝂px D[Օ鉋38KVe,͓\Pҭn}!*BYVO[LX{zJ{wRk 1]C>+*#<%tRnl}xEb%k 0 '@E0u;9-uĪ^7Rg+{ RZ.NHqRIQHX6:SB^Tk8Ɛ  4;0.]猬l+JYBn:9 z W◓[ zWY13r]H%QAG5%R0Nx쭄&%IL2͛e>ut~ANJiq8:QƕH#^~.p'shy1pZ[O&䚒ӏƲY$-ތ3.ϤhD[3*ɞ>qaQbg}1,UFo9 `58~q6DZeKXx~>AҝxN>8xEa), U`WK1pcLvΏ'Ϝ-hD3oD{(dFCE?3v@"P6IcP 3 D|e'KװĽ4?%1?2$ NuW8iv;`i]1[Q``uW?Am9^:h YI’T _7갪/<-#(a.Ds7LF+'R*uUZI n3Es~֏/M,8.~z61IΛp{@.zG^VDMSsMJx=1M[w?w/jwuXm|~ LBBxD̖[S˸oxI  `e[#0`>Ue֐*ζY;GܷqEݜjզǏr5Һn,o]W ~'ƭC^=sDũzӗeHPq^%m:Ã}ʖzZ8@ _8 ଡ଼@T ^ỉ5'STOGnhk}of/{6[C֎`ײBVy&C x&cI D(-z:ԃ7z )xdWFp5x9 ~YZ+gyΈcl1.1c>*ؚ5U~AK/H7sr 8<`tla!Df&tGOifz˘Y?͂xMLJnpkhR.#ScO!?N$3[8aMj}eJqtl@TҒ{s>^[9R, 3 Jj9^KNy+I,V˚a O=7nmyM U=hCr010~ q:4gAO8CCn?\xE#c5kY_}|C=zi1^:ӄ~RQq#K;qP !7ԫ`u9%8qw}{O%#NwIs~>D=E9,j?/d vmxAJ)}QP( WyMാv\*tyH[R>W6m$H>FS雮_!4ti&[r []c&fbѨ:BZՠf*\ +bK>Ix6K綟[T(c=c}B)}Ѭ:n |D9~9vF0QXOudij+kY?nVnv?Mȶ&sJTMx;d*^\j> kQ{nyg'e{>ÖcOA?9XT@m2rd3fP oDݦw|i'J1);YƂ֭GD"#J07-#L=.=VY'~H5A'{LYY 4yh>3@U,.DT}7۞'9ZJYeBZM^ (rMKmWnK9}}S=*}us3KB.nT1 ?Hߏȋp2UFlZ~m8dt5'1PE?,? 1쏞.+R4G+OQЂqJfskm̈Z D[@f'1y 汉'w~zj?]Lc7LQ`~\ekoAtΣ'o}Bd}khI 蝥5Q9?`F!MORx ^cיe|0>WoT>[O:|9/V R; !Oh^~aշe5Dp[}kz |D9|YmQ}TӓKVt}54|'D BG*ŊҜDkU2@(w.>" h/QWI]OuW']\y$yȷ~q8)mƬ'g)HA3O}bV!+ 4},'$s:|fn2tZ23Мz{Ѣ/Uwl݆*pb)?-?.bCu!}?#p?Ai$v3 m4foh6X񴱠kl_*bdfoi)[Q "PT5AԮov2Xiuwc;ꬕKLW'HV]7g4d/u雌br(m_F s?-DS'˼┻'J,rJ/WbA3$Fbz'BGf.G? Y+0 4wh@pO=X2Keh=I˜v nki?5djѼMfc3Cg0*X+y#VNxuӦH4L^<B\^"I^Ii o5"nҺF0bA4Sɥ̆>Z|FZdakT+W,ӡe-n.w,{LZ@rmEL S5S}WN,b6@OU>W/fZ3uUTq,e|zݰ1Zr+;yJmUdϒ8<=I p#@4c'DP.7]ТF! WETX AGtq > 0:y r:Rlr܉wv^ +kQ l&y03DPw&./`mYإA y:A%A%a:qQw> {?2>(N}䢁2]ƙZ^t!o+KK*p<hA,F+d%SBLݚ%+S*[?7a{Шh gƑm )e-mY-L7k6ʶc*9>cvaъ#K}. ʈ'!ݦY?$チ5х Ko?b\|Q2K1 t8TS%y8ռ\ J֏JsҸ>yw*n[;EڱVIb{L?$6ɥk@y8 ]kLi9wxsZv:[crzq(.ڴ[ӯ eD5S-'V-G/ m> S$yLS} >+؉ZTZFZ5v8oё1+ŦŞȘSCf~.jœrձhyJiK|wI%7牰%)Q8iH3'Nl^u8Mwl?YPqX Y h%H,ǾOcosZg?'H|˰򣚁QcXolX x,+tsc1Cj ~tc3#0u@>< Oz -P#TOQWc}ZryxS'#4 9 _cW$})T6ȡ Dl`-4BU0Y;OzA/ <=H1IdXݨwK˄f1-9]|hqaBH\*nˀ`Jw[(^g7߽m;?nAAfdi%vAn'<(hp7h%~>xUIN>jf QDgh\ nD5&#$xK!6R a=Bij}lw-Aa]*O+W.l7:d^ 5ֵ[ Lt8%ȍZfͪEru3X*A3bwI_)Pt~§ߘҚ4LSU2*< W}W}IM,L#ud\.+qI6ld^'YIW%<'5B;1wnsIFy1 :<G[yx/P7!~x >T-~&mۃu;FN붖Ϣ@Gy4[Q-,PK%ŏ0I-<[d\y~O(Q{f :3vMCP n U8:{Ck`:ǷtBQL^ƨWVciL3 EeE[Ϝ2Ph}I <mAN_@b\$BKCAŪ.v!@j<]= Qh{T696IegnӒ8H Fݯ {+`u&scU_saӳPÄwU`V;U :W`gHhBs%u bx-e ]\kĴ)%I(u|PojN-eIJed[}6< ?\VyS9=N8?mW`Ejn/VL@) WM)Gch:uw"Ytːbgq&3|ׁϰrVk21U {i RfAh^$ⷝ%]\d.wgp/X;:)fhSP`as7erY­<3{ًC;IZd9`RzUc%}%eY}QQ @DŽTqJ$Q&rN'6V^mO߭EW7%j q"a; r®m$ѱ"EfR$Jq_ۦ$Ij^(( fF*Ӫ'y*`ۘ'!tg0ϣb=ܧv*Sģ>򆣃W[|TɣT5TGVL>,=4svH<+zpck, q͠iAm,+\*QPL[ P&[Dİ1TtqpJ;9ɢZ m_~]!(v1/&A-{h>PlB>>Z=t޻Dͻ6zg/%5j)1Z v(B^T2Mq%[Jϫ dL"['d|ĵc L>/7q֚ǣLĶGVp3L.sgϱk!;#lԜgYq\i*P;y4"Bn36 ݎ^{Kt=Ɨ_: xGӦҾ( K/ٙwIɗ:Q-7z'`U7Qb+sJ?㿸JGHCp mDzrQ(ፑEua_h̻.$ ןP}bb:Mo[*‰K>'x7kgo׏t_:|IfQ1T7;oA@e&L/eo@mPm9ExlAVY(i-&"<(X̠z`Oa!&m]1E(?tŏ!J74gloP*dnk>üKNP]u] #*/5&uPsFPA5a;^7&?׮_쎏f|s%@dP!Ͷ&@ i =5j-HGN?{4 -P&* 2Ցgn:{+)ތu=M'p 8[lݽÚу6%JuβBmQ,-SwQP=UPι)BIާj=PJJg] :od^aDeqxAqE\+M@S'%Mt-^jvFp}2 c'CrD,oRda;0CXamIe`TDE{9l9b*g}Yk h{/7ӢBYHӕ"&L]NRLVGO abS);k䊲VD_Qҡ=5h{0#%H48zyk&v9}=(ypzkM ,o R\1!h.1 Dª#TAEڵ2/t*5^:a[;\7yJY_%IcTUr `nݿkj݋@IۖwqK떣BC%Zd~mDt Y*ΡSwaTD!Nfk*!@ǣ!B gΟWEj颚A&wSEn 3 %͵مo= )3LǂDcC+߽~:BF*}ΜQ8 }r uƩ) Ps, q zcJg^ܷ{s>f~!3 M_xi U(&o>\!Oyh{ sX됪{;p/jQ?cOԽd[67q&V:zLKH3NTJfӒsy27DA۞+(RЗbݞe"=;~)(YۑFTu5BnEiRR'*gۮER!,KG<"%0,?/B6/v2SUDi}&:wY ":lqѼ"PS0 s\ K L":>B.G ym a*M2ɣX>g_E]$=7kΤ8fvc4B<\HsY+$:5椛FP0?,0_E_),൸-~Ƞ'(&մ>,@H vzFR06Eù5ld{L9=ƭ"ks@_"W cVĉ:C,wtKd+gcQ_{/nCwOU^Ƹu2Ef< ?DaRMK:H~=&rjZφ)Ê/ԓi$ם e  I@Ki@;F Ň^4괄2BUyaIxP~:> 2 0y}mE Z1X?]M/J|H75i_EHIPҋaB3%tbB&WS;!JMod  NFҪ]qI/_}aӴYOc'qO҆TEB=ұ<@+5pH&nE=J\Z*fj.h[q5k@4Ոy$r;. On= u9V`GUuppcƹDIĝŢ.q.I]Wt+IW;d٠ S60eC UEu R1Q$2O.xTxg>veܱ]yc$X JqO;G" l, N:ݫxYg$)"r5Fi-v$wp=\'eXd}wE47O[DlhAb}njMmtK.$oZIR@Oȣ(>/qŨ@0˅u@9)Fq{zq/)zOWJ gbZ1r#C5F΅18Hڳn<_Ʃv ZGa5WYh.6Ft)%[iKr̓luܺzR _mLbwA+9uפMH#wcy~'qUU4nysbn(5fǼG{.-ESΏ C÷naJ F𘅤JhDqTrvxQ@eM.Y<+dn XOxj`v~6Jߜ.C}7{ _Wg!(?45bZlU-Ep=&ֹpw &FN ӵcOL1x\ْd<,Jy7{T뽯#4PI$L?i^ֺlX*Zeˆk@Sh"|(B~R (qo>ir2Kp=}$PXV֒-?gz=IZfoʳuE"iS$ǣfomn/^z_:QTLiYC)hA:N&TG4]e} 795UH$ kf yCrIWqExb4c}䄍+@{0>=kBdO #{;(y|AN07pfRd0;;ut'J[Udq1D!62zVR 3!I05j^9󦑻덷jH]=0\!PH_l<ؔHw1v_&x޼KաOo /^O(wYXuVMJ)]DȲ1`('5\b 5(>`;><2H3_܈޲(Zh`7",89_,/yKl:iYQ؎ĢMȶGc*Lp'Ar8I'"ҪLjaQi0E}ݨV .%\]χ߈*K>&PXgW8w8|I z*RDOC0R/Sv̞}R+n` /`PNv>edӵ 8z )cNsg߯́|]);-˿)r];d4=ЉzN6Gep:C%>2qV^̡Rj?rI=bһV;"]-4Ws;w]l'M+gsz!f6]qRAS[9nTO{8SPQ5m{Ra\2PT? 8fg}x m$-R,l}]P *V]Y&u2v]P16Eǹۤ^QGPl8hZx4]a?.ǏK%oL7{T~!F7=cf8Wt  h,}pz. endstream endobj 1192 0 obj << /Length1 1630 /Length2 15980 /Length3 0 /Length 16831 /Filter /FlateDecode >> stream xڬce]%mۑa۶7l۶m۶me(3lۑ_>Uգ]?g\kq9 1@΅HYN^``lcI`!'qXۉ4D?`ȉD<,-\Ԕ5ii"c@z:[Q}p;\R_;D."3KddC"YK3މ "{;SJsf%LdD0089l->Y:;ٹ큋=? +!';lb]M,\FUw.F.v ٛijoOIE],휉\.2Z:;y_i:[ڙWtDNs#'S_t$ߪ7rpvg.3cmniϰHٙ13n_ gf&adjogId 0awNe9$D7qF!=ZF!{s9s8nF6Qd\6E0?mt*ZXٿjv'K;_mֿNLL S4GC;_<Nſ _a4Ms7=;3"V"NffC0Z҃HkhLM#;ӿ`W'-?{`fobҀ?6#34 :PެZRPgUmY2zu(Ms41aCٟ.%,BEA{Ĩ_y}"Τ~7_ ?uB@VFgރP|~A|L929>6>xKMczNil ǣTp;Bx5* &4QfE7Rc0pMznndռ41#PWyaC (Ov,Bh>4glZh&ʀ'gQ CS;~!Iy}>fImDnsEɍœH=+(fȋh[띚=_W5ˋ*?dlK\JfGv\< V㙟K@ Kx+/Aڿ A~Up?[-76FQk:ZĆz) )TI>\[*.qޝ˪ lh9ohZwΏjJQd'sAGL~nDKӔ^>е !!vrpl/6rvՃnUm^>,롏. >xՠV?!خR\tØ)һUudCd9m{!%zݧ-qrAKVzb!T2f}6#lgKp0 8,~n;gCЯQtrH\ȇ͌?a'` ~ t}Fu0ˤEӖ굾>҆9*>qf6I{Yl2{El{Q` (5XXkyrK%3DFWwee\Czݨ׿jlݾW~ĵuUR/r)Tߑ_v b+ 2wÐL(N7bO.E$#?uFv _Uq@6(2p;ߓCHY샨EG =9D:B!YD>MaWd7+VLq /.d"}VD ˋb4򫀑qx it3IɗDP#lnbY YeFy ,p,)ŦŚ>v1۳bx"KV-fz?H3g&-iNzX?V.H#va- R􁍆7Ά+&yt?{ QXbM7K;bM"Li3!e U@Gj?3G̽b= 72 ځJz$?ZW0(p'\Z;Fmy3Z@"q.Uc~FuMQ+Mt\{GNCd $眏wE<о+,\rJC,\0Rq$C `&n$d;z,wS{Њ ;C>$'5񣿉KuwzV?1@2K 3SOgKЧgfƅ ,Z ]fcv7:jA?Pt|q*^{h΄d,q+82S~TV82ʂ8^M]1w׫J )nayR! 3~NPv؟CҼ2V/E!Ь|dR35y|GHgECP̩56=H_y"HF3)"Lv`PohL󩌥.?r=Diq^MP7%Õ*Ѡ=]^iNKB ac >ۓ}\r켖5][]^x 8ê 䧶#KFGm%LX"OJs12ƕ z2fdZ&܏$`}R7ۄ)r%4^zK2d Hpz CrxgI T@.jGz-OqO|wcIydړ!K?c{/] 7f䉍&#Yv;,50ޜ{l#7p- 5V+ߺLVDrW<eץ:~PQ?f=1!T;Ci=VCʯ0یGv24\O 5lĔ탛1GGC #K4YPMZ;EHvs:S.OhxD0T-ԀX#ڍQE)ᠸʼnZ3am%X8߃Ďcb}L^/}#PI% Ղ\"Em!aB.5sN aƇ#f{TVv]657y}h ,&c|ZIKqU$`TD˭sK}N<>UHldgNP3][^wJiy~HҌ|!g;%BE&auehfH\Lw*C9L$Pt ԡhz>=X 1jɂ;N]^P vd;IC N'-5MÆŭS6>A Nyg&ᮚ̭HCfT\VBűgۓZL&Q- W: 7! fʈZk$_`Cw{^V[Lg dXBQMA|:b Wox[.JLx$~sn?$t}c6Џ@q}8T\y@pn&v<ªeJ5NQy6cx^4QP*  G8CL#NĺFK~{2 37Q^j %B yn0Ʒ]ÿ,Q~?oN@:9(u>O~(#%V9D1Y|nuĿ/I߬EڕkIu,ŭZތS[<=;F7L%AΧ2S|aa8\ga4X6 ŻJdem.迌KݗrۄبَZ) YoKFƤmA T3pW#3lZR8ـ'NH14|:sjBz޴5o$$Ld!<nA[o)}&ˍgDz9m,Z<}2Ķrn,58К4U >Veͣ&. vԁ#@ <++Nb^bsXK)Uz6qsztc:(0E;piFbsZbS`nb|IߜO.w~\մԃsgnM]o%E|' xѼ60.  ~ApeJuw$G|y0nQ\{c=nNX{;|T]Sh΀E>bQ>'=x1Z~GlsMZ~S e Wh.ʮ[ZѰ+u݌}q`?-Ĭ}ؽ Sٵצ;^?$!$g}hVqbI̋;]M !9eQ4WVAm9#hpFu}>۴9Մ$5)iMh*Gi+h7}ω%KNRC ًk0d>||s-BV{)>ĹJMaj+qEѡu&*ɕ{NQeC \@s&Cjlj&_;::z5Vt-8֧T9)zcW_C?~}51/4B.'zN+~ٗ% R`3 n\L"WQ$+hVAxm|>w%lt2elh$m\:v uւ-D@vۮz/;z\_<þ.G)VZ~\1fc[i  AC8 o6LG;-O9" [F-erdKp+ %} _t&?"y M""XBN}9R9b8!n A?ROї0KoJrY%R0|/mDZ%=]Rطz׫+Mca=HJUaL'Hm)c"eِ# Nhu ?.jyZ"s>mg3Bւ* ~sT=ڀKy,e֌2鱩REmk|alYT#ξ&oޯ3p5EDe5Z =.3 ,K^{7Oބ 2?/, Yaqfe;3c)FnA HT<-Z;|eЍd5X7 D"LņR>xJ_hoS °']X}M 9fq~UrZ}L$VnTf*颡[˥!He4~o1`SirHvjO[GeHwB[\7f"< L7г7wg-(JٴfJe{byY5ԉ@gH]Ct%u"ĚZ(T@% omju[ 2d_?_K v%>׮zVƊ@FAtE߹b^~ҍ,*$KrLR[7yw%[ pOܵyR @;lt[~/l^~*V1Ԏ%t_[dC aA8P.ď+_{<|l %AܽHS\:L5oP=XG{OSG\XnXoTk.8VX̘T(Mw:_aj&WqS"{I/Y4R`h Q:iW^]32\_5^|}.Q$x~*f,PL"(9J峀5H~)PQF-Ψnb"*˖sl I.|!MҌ0-d̸~[<i1}w>X;6k˽jXl; s;'~VeD1; fF)i>pψĕN0b>Κ?2˰'[LzB K-0l@8/+n` NY6 x#XND\ $;^]@nRϛNK;ԥ*چ d3l~xj P 2t^d6D7 dx)J/%ŀy;D>h4_9௓: ;7Ag\$yx,{YQANlg3^㹥V7[7 ;!, jJec>Vϟ0N72h/ч }V$kpfHHrb&g@Y$xttV  k0BIz7]C0ɉ呠#R+o[BM(5(r@9'X)y~S!_ݏ%ZeDa`B6u)߼5)l_"y <'M47L8ߩ:!io 0j f=9\*Snݏ$E*9Pj08iߖXިG=tSڡ،)FJA-E]SBG?\1 ,ZfoO.8 '7,1g=~#;?BEq_8o)1x< K{LfC ]`&k;Dix<EvQR59LK6PqZL=9k0*_ܫ_p|dQ2a:GeXeK^=f.i,'~ ч2Vy=yM8 ]{k:"Ĺtsc͌?nšGfܺ4!\+}(e0CrzQ\Kқ}Gߘh~?y ~Z!KIu5:b`"V갨I,Iw duSoRA>b;87G WSΠ%c5fVT. h$@d>xm eqMed[F&븟Wǔac:n;~v[TA:0~,5ŏI_HRmI6FbGNS Ӟ81}׈Ca+ḕP+WAĿp@;&}Ne"8UMψ%?1tWh{BѬ^/џ<бE4'$}eOS`FcR9K'XpFqTRxu6ndJ냭񂻤3`tU"ZQ>exO:"9_QsA[xb YdcDxW0$[:gO+ :<8 Ќn/ea5+Ǿ^{T`:xs{giBeHAVaMv* 囀f̹Nn%we;!${"ȽcI?1C\{Gc7oVnco$ &ekݽuK$hl=ʶ&0 캤iiwIܶzOQ_&o;ʡqgtדf/a<ŗ9d*GPθ8wNPc4ji?^־gƗMAd!*N 7tgϹQXe6^rK Pt 1~[r.çC'VǫM̐:΀gC4t X!eI!lݹHueyݩoA6j) pm>"a;xoԜ0 6( MjTنXo^M$T^7ªPa2-;P&JG$EN%$p# (7tVl$-`;srJ\ÿwbĴRk =Jcut/X `7A$坆vܞj(魴v˳d"Ж>m 滒RvDȨpQW0}S+߅6aN /g2\s9ISopṝCN )nWQ;fU i"Dp="&Ċұn<샿[V"yL|cpѝ5^0H'ocW9g܁u x46Q%qC~Ꮾf:ÅT@kHD5#inZқF+Zar.b'p 5hSLy y(*~٩!Ժ})rF,[;HnRNnxӢɠM$ZD:8(X >b G*a$]G?ʹx6v<%@:OF[Jr&)Ox7W5F$/dYa%=*)p-cNӸb. r=JОdxStZ9 q$5̮ Xђ6x©br?{N7ދP#}5k|<NJ߲)Ha2@iB~o^snLN[>3bL|;* eV ]Qs1٪r6aۤ!"wKܱB5x KhWJ0FE竒HLϸ2ٵwVJU $;l۴SgN\y{4;:{ؓHHJVM_UljalEM:~hJ_ %kkMpՎz+緛8 kٛA3 ?8WUɬszKR?tc f]~Qv0 "NQ(IԽiT~m ?ԳX*]lpF%h^el6 y> p͆*Lmi/5ùޅaU1O2y͏W b%ʟH̓njܱ5ÏS 6 4So82ehdK]M)P~˱ 7'$\E(GI;ty?dxΞ kHMFrX qW/:.}bzɰk Ψ3cxf2UI&(ځ[#M|~ Zx(j`yu&+B nKpD HY,oU3Sz1M^Uu&vtgfYRqK#aJ59T!!-Kpy#ā%G+Tǫp4/:ݓ HUުM`KIDt7WR9Zv7p YA܉ӣK*},gcR%6 p]jc+C[YU%6.@:E@?&B>UvǧKHٰ @*z ZIР(з|b{}lວd:\Όo&=n6:r?y;g!F D㙀t ~IrNA&!5bDYH@IP6ا,=vψƽ nal9KzǙ[.,$ʲ 0UZ>[ed~~#VN jq=`yZ$n k"D>=v7 u_K"m]"~ܶGS/Ǚ%Q+aokPrʒ[u(l3c1]K 3tIt#cDݱ-v9"v0)!orD,Yi]4<' nK2C$y-]J|5zN'؆]DqNW'˳r =umC~V[n߃GzU\tiW),ʢ:wx3/i5גՄݬ֌T4$Ь2$^Ufu`B+qxMZ4dؖJ14'pۍ]?x(98en"3l/M'H.ad[zSd~uY^+P'} 0/kaPt{h#BXhݺu&`JHBjht7l+Iҧa {'_4i5:| dm,$O!Ȫ湼EY S0ab;jPAG̯? l>06PeP!Ki⸶ځrn{> stream xڭteTݒ5 ڸ;ݭƚ'Kpw C%h>̝u5s16;JMu7-o7bQ{|Y8,\|o -?n. /[?O#d kN4̝F -]\翇Z,-B2>g OHq95jՀ{3" T=׆6M {/;*28 H 7h;L3Otc}Ͽ+omMk>#Nur 3QyQ9cX7$wa5A& ^3& Sy4Zx$MRX">RQIڞgpmF[|: :R"7vOm cnxjJ*~*` ȺOk1w o!A1 kg&7%n3F[FsQo<`c1TLs`;WEUlUM=QdV/_պIx4GK#0fL+57e>>G2%?g[:h+zКV:vkrD2Z7?.q{E}ȯ}R*9z{JN=''(k-U4@@W$Jm\+6\bzGW/[`!ͨxwZ]eI5o[q]Ilr,*XUh $,dx!'JcJCW,r*+!=o"rΖ$ p#!t wnd(" 6.cɌϕpwLxIԋ8D~ˮEUd<`x=3)S]{J'w*EfX~Qr99ɱL#<:̹7v~cAWq::۪\0xQt.UTlȎ ˔ϰ їmfkNj !,~ɸޥ^ڷ*課hkQ(4@ I[X_Q3dDZD{r"@q zY(?/..[; ppPONQ=R MϴI{-\ 䕩+9zбJ .ȫN@,R6E)HU2 Hdx nil 3:"W2H'?r*P'(xyu1ŗ#¿;[Y0ؾ- Y[yUI> cc&;xЎ?# m"'+gq2/+\TsZF2Ohv")~M3d$A&2jQ#YCGj*0h3K(@͠I pq=#M^BT붺!|D}1msTMeh~/P{K$w["tp96CO%FDqwjL% 5% 7zYl*Z9>T͝fk3.WaԇG%Q]HM3 \Q=Zv\nu8ëmClҖqɻHBD$.:cg\pYHhݵPI 67 e 'W/0 uvDPT6 h:c|9OQXmp ie( hϴ0=[CCl3E.9jF}_gAJ@Ͻ2"4 GyQ(7~-qިϺa&N햡4IOXra|HٰRLCFN>6 ;/jpGm>D\ 0 fvD5Dw.W0};=$!K!;`pC/F8t{Z'EL? kv|p#GN0PFqW&eO@=Cí7Ʌ6Cr]Q*~PQV`Ρ<5|ȟ>O Ē;W}tP̘Vn|"EYE+`!Wy:=O\Bz\N?4]myC`.11/&s3ҕ,-U|_$A' e>HUJ kY-IZVUyZc}1FDdRP'vInQkN%pg&F Peei3V7 NjG9<#9/ۭf# 5&d݋U;7Ŧncˇ'Ū ז+.}+ ]+q녺.EZi {]ΞA"{.sQƃꨲ5cB#b$4U`Gg/<0'%Kr~3Mv{̿x\eZhv?h :jv})`Q9]Lis[o>Uw=g63;`4TߜCn+z`IZc1)zv,=aB n`쇨T/@gҴX#Dc9 ȳJWڗ4NW:7osķ_m3O0r ipLa/Q8VӷE&sx37Hsp-K{)f}"5<0#dgna35;!lS2ߘԋ=t8Ɛƣ^:W,D>1~E}pR!$/RRNUM|X%=(?4|g"+ VM NLa]Fu8^t{Y1㭲%]yr"SމXO#[$f+BHgOv,\[ @6Ƽd$$ ],ty /]">;lNq`J0}ᘷG$Q݌' )&b4O(Vմ@&nek v\<I7ُ1B&1yj_Ravc %0L |MsUn_P4InK|xbq-+k4hxs(U{8E֟ ^f.񰀇kcW14A5v>=;qТAur-35x"HɕL3KP Y,*mNEJ? 蠔S9j eQY ɊElZl;`xm%*" j|d#(ɂGE?߅dOy!`R+"W{r d#"S,%VG3+y 3KNV  >H,]XSf ŸӦG:CdT9,axw~(a E]UHT0y`kV#sR"j=-GojobO^]9Qqg}l&OnIԯ2qv2xy ,UBN%[H&!gto -_(g0s`nu:CmX$EV%S*A~gAay?&ynWS`;+Kr趥dt(!s :zȯl_ U(#U\cD2rkQmfO5[ i$u,;͐|0ߟhrd\S/<\:Sx;[jaW!"^O\=Xm֓@g/"R AADɺ-C^)#%Ưz럱1('iBiT|V %HvH4Yx/J-U7iLs0)0+-a-d<#ۨkh=moiQ#m QW˳' AK* ^zŁY6|t Ӷ4eUeoj{9J hv1O~X4KJ( ԝ# /_M"oڨAWA<=(Oi09psTՂg8:ˠHXPp׏++jɦܢ3h Yl[٦Hs>JgLzfSef[Yk ?;ZSf 'ӵ:43uwh_Pwl'ϨH\3_Q ~J Qs)14^A(l:!_JM(Qkݲ֟ep*z>[}jm`7ې)Rσ_:I_ 7,), ܗ3~?^r_^w2Im W/@o0`3z4mGŘ]K_s PQZR. fQ )ufQX7FMT~gWliGwn}PEKW {|1rX"ǔwɦ1&a >fLN&Qhe3:`9`|PadJX5'M-n)qUϵ@<ހy(~u韆gݡ3x3"ic v ?}RC&@^ik$0<6n@d}pb^8,1"0po9BFRyWOH:ՎZ|+Ы{o@ڲᄼːf8ʇ5!.,Lj09P)$>d8hSeu8OQ>s|(mztцtSn"sng+m‚K+:Z+' )>..))8 sWM,oyAt#3hlʆbE`>hucwG] bq B*)|G| ړdgg"=*c}ڗ[p;jO93#ZNҺIIyta֙Ca2uto>+Tv0z .%ODH,2#L&^Feh1U$=fcfpwu|y8?Taߪ;bca0nVep5} ?*#Aw^wp2ڮ՛%piKV mhP%zh|ٍLD$MIeZ ]+N^.rGnXm ,D :i|=6TԳGH}Si N 4ЖBǭY.r[DZsaƿcZeT&.>rfdNڟ7קM݁{?{9%>f ƒ6'fshN'o_S+սy bF)`$RavR#fxǓKRW:8fm ~rMmagC<v3ei+9{$ ST#J@0$=v86rmѕɂOD9NlVt3$V޺]IŜsOЂQ])hDΌ`WHэV[rD}7!-h bQS,ϪfQK%oI[BG[ Ic,*v_HGH`n^aP,<>賒EʺIb+P+:XVa Z2䝺cC#Cw]ʸ|2r& >[B)QUƳ`z]XO4͞8ym}ƨӻ1*Ľ87 Qɍٛ_ &+fN#'k&X] a=yW^ OԘ`uEaqj$a7%׿+0]av~O?ыt4ZS‡妔+.%lV" >rkS+/TUt)`/cGlJaB)`J v% /ܯIv e^^O; C|6,& f̭md4#bXҺ4-87 G@;3iKܢg]w]/W\Dʫ˦D9/ lUS{9"Ԑi2mN6U` 1mYZHZzpLG-z \e -ZAx8 ذ= =qjKhb d_,ɣ6儑F ES. kQs~,JU H1rm'm%(d\'[NtE?ecC[p " ks{RňĶMBh:*"'_0sݪ`!L9G;Xu4cPab8kovD3oda):E@Hoid N}To=ă2傮@5CEVzM0`}yuUsFtkvct mxOWYϻ3M~*36Xń0ubnuru*Pu[#uoS\wdS_MpMApoi *\ZAa,ٝe</I"H!1N;a[/%Ħi:VHQUk{wvp;`(Bu8Z8 / XU:.jrN^mbIS!"+9s5gĨo-!,3= ܽ ʋq>|Nŋ_3JrF Yξ4ФҦ003<؎Kk$[;7Ж\/ό/d*0)ŠEfKsMi[W;B~`\|'ܟRFHv nǥuyO+6Ƕ1_W&osO c9_sqo;YΒz.lU jQ!MGڽk;<'ǖ4?Dq@w} b1Zylʁ!SCu'rp7ƝUyA%5󮓴,q83`TTrIgRAMrs2ы5C(3?mQ_#"~ 8ZdL6~d"AwB.wZzju 'Sf$e9ɠ~>J+Sb=6tk^_ȬBKSu1}:glDM$qW۔j'G}JS!ֽM; `TOhxܓO&v' I5s`1cOt^ƚG5.o{~X'J30Mh)Y3իjUscx]VߦU,% ORDЬi盎P(,M_e=b\tZL !\Vs .vr ֝x'[ :)#ZIxNR\dvqy OY.n+qruefDk}5ЦLu-]ko;TW Dmp/ZS_J-,;4(׾{x>y bEꈧ>6XOcKi]ow!Gѷ/^Z`٫! 'bJ!Š0PԎ={C޸/% <A`o0A%Y]PPaNƴ9uk f$!Aƨd$BgɹܷaY/ƱHipyTnRo5}̗ӫG__EG}}⯏ uIr֗YO M\3Ļ͡1Y->.|Nlx5s{)Zη3>+D+{@̓lm ދ0٠J>^iS@)O?L(^\>E:IhQ&'=#} +eD{q3|} 0{=0MbsǣU~caDli?WND"3|T񒘟S4iţEʩEþK sNQeZ*P(?<;_;55)16B| C\FVű2r/X7ƃ_]|.YgElv.1,h{b;yt?}w_ rUۿ=ೆ~$w6T1r_o q-lW:.r7DDqʊm~ٗ!b*[;ӮE}k5=x_xAV=;HD1Njta꩚g>0٢sX_'jKE{Ic+e=)-*6a;aTy@2r耨Aֶ^k)gc? =!7Q %!5lCJ{_H cF)5BlϲTe1qlyw9+-schېyVݚzt3 NJ'B v]=l[ aΜP[\ =E<4|wp<y^J E Ĩ# դf@;퀴o ݶ!ᘒ+冩Pz +Ћ7].$0ύ$5x}b6dID33qD kԗǬ=BrvQev\RuvWFsgG>Gҳ>i8tW."`f96<-pcL[]G~;Cvhqr1n ^eO~kS"Nv95B,S'jSXt8tINz䛣C<6fO,[f0aa9IO=dqp?JS-~QVNymZB-(|;l^'[?/NJʼ> stream xڭWgXSkF !$!BIMtAБ^RDt (7x̙5w~ok~I MĔ (': KN^RWLhaAEǧpR,* ޽KPAy0,@BHDD/ɕ !o(Ebq  @pb`h7h@P4. C/' ЅH TB`J #R@ ǙAW(vc0oAH,X# WΨyQ8 w#3Da0'` ARhp(GbXC/'(x @8824w^8W4BCP G㾪_y%{5?cc1P8)P WâtF!^!\  @Τ(,%@?W_iѿ\};r K2.j L@p1@I:"S ǨB!p, B[n@8kz⌀a.pwCP$;r =5}-+ZkFkohz(?W<ʨ11iɻ P6-7?HKJ?NvQCQ1ܜSph\{_}\8x(!L:3{䚞b|7j$(5}RjN|2BnTwrbU[xm!О e ̣o-&a_Dm?Hh-+i4ldl_xN>"&? ANoãO<.3sU$'=cP3#|#V!1 ?KĪg./ 4E[j+Q۠h `ŶDmҍ7a['cBV,) /FIBR+:DnD:Yf80~Qc]=&mi F~u跘d|Gqsc^s5ԜS~C EGh52Ͽ-uiŋ3зMa.Csu$DW2bg@  2 V΃ a7ؿ ޵eLxEEq>6䩔e#YOaǺq)xP"'\g@AR5FEH(i0`e&ޟ s4*Ĺ6Jtš|`Z7@ꦲk2_O_LrR4m}CR)c;eo? y>40-CD$6{{3QR.?Լci$( K^NHI0]7I-c65j9DY)&xcEL_O>[. FY`e 3UInBF@j\ţŤQ&۵d&*۶;K[AU7$_p;A#LGmb:1׹e?( jFNmvJPfV)z ҔqO#}:ySr3Ӿ+iqTY'IL _AEx?-7BEǻ)ߙd}.✉mMɵ76.Y/w+Eޗ]&,O`D*l2!_>g^ih'2Ҧ }~D"@cөQbc4(^$㥒kv Osdd㣱>9}"bFäDmOLݪ%j֥wjl:G8& Ϝ_q!r h4r5PR5)mWT3?'6ٜŹ.glenJBl4]>{պEi:Nl"sY ®q̔(NIgV'$ߺ iLUz?jٗĕ$׎".k 8 ?=OJF%).ÓX) ]Uڼmc6*ޙ''j_'e>=k XM_[ ml ˅QP[^vtÄ!Ō@'k®U/An(6ָTm[>h5oP8EɉE݈fajx8*'O9q7F ,Yڨ@;h3> `s9x"-Gq&L'ۢh\-I+J{흲O rsDnV|.ZIURy`.čh qwѰ"Zݙ/%Dx#xYcds43g,t Co^:Nv(%06TToZ}2bA^+gPYSoh7]X|Ȯ K?]PtZWUHP25hWt.\AaަWE\TqtM4sǩ;/ H!;9L>^ʡJYir*X/aƐ$]n&mto-Fk0a3Bñ9z|Zl٥̜l[jc2?d?UiP xT 8]:t %@'$; B읥K)9}0š*[*[7 SSҰ^HKww1znQd fʬz^l T9,V!'t5f"YfD8OɄ}Gj|Zk~-0;hC ftm<}ej'"Y}b/毜H*Z߬,Ā-j1HEygV!gRrm2Ӕ>{J u ,O#sУJڧ$d 3ihק U^-C!#R!Ma΢>A֓OAD(05<E斌sEs.;Z0WDd7ވ6ګmovy^dx!e%IJucOs<\cFk Z/Y\Res]:/,hg"4hцmh~.ܲ 8^2)[7-9"pMv7Ef?*p(OqZcWbG1Y5] }L\u}/YY$8.|^y3X+>x02TwQ2|izueN cZP怒צ޶|je57$]9nK(eVGN[JxOiW;~AL-ޮHH$#$S> YD{hz'kN;y7 @3<ՠV">GrϭxFnit[D(ˬꨇr Mzfv[6Ǥ@oUy.DX$m?s6q<8$OXzǖZn|TLO+eK-o B[']3 `9`ab1}lPN ;Ď7O=!>mK/ygQc+Pz^VIJ'0TD*R&X#-H']?\dTklO⭿)( w&Ofgr!Lkfe큺rԂrՃ+©-.5il>:q?yòl558ߒE`> `eo!Q_Tzb^aM(ršUW`@G Y iڹU/ܽsYxozlRbXEDS臅y7T5aKE~K 5(|h?&?Hǫ`Ac S/w; 4m Q{u bAe ~)^*Gh2GTFQCIZ$wҔե7ιI[fp\ 9"QZv&/lm>(hTx<&Urp@1yIh,t΋w2ڪ3 N\gv$2`O=,yy7Nc͈$?Vn[eeއ{=>[>4沲AOqcC$;&Dh'nͼsXRzɲj1E9_k^ݪ><+? yo?rCmk/1|0T͙o/AaM4Ujg[y\K[l/õz|uRD"^EE=Cz>u-e_1yhA2{SӶT?çGrt T0i!^'RO Gg>ܕh鰼փנw-_VC'o[8jx-#ÔQ]k?^m%/xgсP#O$Da}#Mcu󆑼, FJ!@z5;`m>#i4VXS;~}"|Z/ngm>fT%k4b/܇3!CJ~+)9s͍Z{Yk oA/A'-ʏnIk*-6U-3/yu'vn؜~H06vvx~ɔA)"靋ؐ 4>ɝ瓄JcQl{vaFGz$ܔe{Sن͚(E\400;݄H&PsL (C+nԒJX4P5Ķ~>ڣ'`<_1çf u,xu^>STe_WӴME8hȾ:[~]^8@0,׾<`y(2ez+GHD8s/=NA\|'#GW|Y믻Y ^Sg62@dEtskh YF/h\M^A2=];{1(cp7mIRU!`KXg|n-1vt|і\ޗoWq:{o+ALEL#68JJyGurƎ6Zne)v>_ Oj 'o|ߒ )LAXj}@<Ϟ5]ݰ{.2p vef38j[emL$:օSA× N$'}M`ukAS6>/FT>~Ey0v/#S2s?~ endstream endobj 1198 0 obj << /Length1 1144 /Length2 5575 /Length3 0 /Length 6338 /Filter /FlateDecode >> stream xuVuXT[A@:KCp`aZ󈤤tK)"Ғ-ϹgzzeF Cah (&4{AE*((DoxSg4PA"2"R2⠛؟H wG PqC"P Ch#KtSٿU(8cfl/7:U^pu!=~eЀ"!b jv<2N` &$z   x*ui_ssE }-Af'7 B=P?Gg_)M=_0y:ݼNЛq F/;b;zk[:{~@!PͅB7P6?{+JYy#)qʍ? HZ'$Po&g>P׍ <JHo#IlEEnqW_-h7qM^@]n?-0RA!poG?nA ^_,P$g wtE@n;E@R ሄ0(_/B݌ 9 ~S u$D:FGt*1 |0;F߹G膽$F9#wN!`"tOύ?ZQyӥmiΖcdMz%fJf:n`;m<ƟsLԉu/YLTNibߡ/>*kiK좦\Y{ /r/ٌF/~OIDmx }qv\F8jBQvYi,oXɋ?[3!@ouZaߦYo]/rȻMx˵?ϷGvH.e+/#!bWZOZyLDm)&q|pDޣ'V<:$vm\1=a* ~CU?`"١KňL;ߎ]J].zP"aRw=+O-~k+^1V+lHTx.nzb%E]- j}GkI֢<1BnZVGNP0M$䈕[}rMت#23O)Dz>.tae5pp'L9O%OkmYHLfIxy;T# ~6C{GT뛎'$@,w#mFSܷZ7gAռ bQ b7-Nզh奫dE^IbdUsFvN!qO;WohJqXQB4Lu쒂N `gj,f d=ՠ34ܠ^XvZ:GEܢaީ.AQoMzhb VbT%Xr8- Md}d&Rki\/ vEvrJ}v;Te?k1+Hf,A2QѲӖ|{ :W}KnR_lu߉W~o?h/od+?}8Q3n uh7N![02e/Z~"B@2Ad)3H3G!8ՔXjʦe*6Y,Q;Qb_=0ycϵq`Fde&#S6yv*1Rb8[Y/OXK5߇=W)8NJw)sL~7?~rN3&A"xh$t6LQU (#T_RfՒGȀSʵO2O$cC1 FW֯5-|UeiaNzvdݡX),+3R*I.2DUZ)")4SS!3BqY@}y=m\7`| fJz4LP]&l:\+Q>}ǫ;ԇwq7_^$m+G%^d.D2Ē5r(IHVU1j6wj4VE@o60N$_3 fc`'rRVŏ6|ܝ9MdzU-IOm`JGԽi`M|PAc`s}=i3hyuw[A )SvQ_۝8sJjiEM|F4d?-Ӏbj̍Ax꫑9U9 o pk8P>K3ܖ2]*˒Un&}1e4NRX$2 }Ze ]e.~:˰zVa8L)H*)MSrcMƍxn5q#0ŏtW9p؂,{e!3K&EUΖ=ޠJ ʕcJr)v 5 dQe}6b (ٕKXo$b1"e: :{sBsu:g/qF4 o?XzD|i"]'OC K))n`Vc暗>x֤zV7ǭ[o0PX/I@]1T1{_J)hfk@5,Pub[Xխg>msQCU fD~zݕ?ʩ!ښ"0 *Y3 A:\%W BY /m3iqRM_ >"lSk:9RzCCI}9E윮/Om]ZabSR>II_Hh"iF S\4iw&5Q2spd>wT]2(@tjmζ22~4}§$ F_*H`ANg f̤Xŷb/G|,bP^k >S G2K&!ϓE/Hthӂχ]wx`FcTP񮮘ԧP;N)ĒLXČܯ 2k4ĵx {L!0ZwgZF*vTD?A;Hg OZPdUu[2tM+x0[flLu,2V<(dsiBq G X|bݕO=|L .dl3ֽ6 Ӊp퉡^ AF8k>usk_#ͱ5 ȏe" ^噥n<Ŵ||ŷB2~վ$|5߇m(( 7̢\蔡?f*tMX#3{S4Z)ZV>g\aZ ux TF@ 4D:,J ZkoULMn'k%d1թ.LS`cXcV)rowGz/ce|ғ!E]C"zR$fxTl8%9 Wm?K/`GQ97\EHmZ߼^yo؃KIAQ.b$ؽװZbشiZ,v(գo-o&ip_(бXmf`&w"? h))^$\ԇf}_=o =- 4|L޻Ef r|O;=q8N,A4_:AAm+~>; ʡY(^W.?0N䇯87 '9_2XnJPu%7k/F;hF-J1*mŪGW՗~<,Qr=OaL©,ZLH~> O4{;doNicXK]VڰcWT_J8zzL5JM=T`t2,4)ݐġJ~սquźnS?^;K@")R$G3D.JqW`q=T" B6F,)3/0xpb~g^SsYAhi1w-l"WWV1?0fO6s/GŰx -]zhq~N2P?zб(@]g#q7{*c[&:#{0MxT"`^OWtqDT> stream xڬctem&NE;vcΎ +mb۶my>}z?cݓל׼ZTQᛉ- oaclg#o# 4s PR: Avb /@hXY,<<<Q;{wG 3sZMY?%C@hmgo @@5 %%/WHmEg#k c1 H0sX054'ƿ9 N@cn@7c?*z=3 `hh vdho0E;'=7dW 3kibgOI W 2unr&Nֆs fh/Nf43t4:9 7?:[`rZ2"i Y50[nl:Dahbgk0"0ہP߱G-Flm-ohwc-Y46C k?j7Z;km7[032[h hh26Z׿j&@Gk [_^R 3ѩ[[CǿU@[ /UϤ%)aPTbٙ?aDD ,6vw/ NfCbϳ! nfU?y/amLI_Ύ [5@ayΘ/2%=T=0&9˾N ϷҮ'%dcG4P7׮$E.79MO:Uݾ?^1rF&Nf1%e766GG_r<_,{ɵh`'TG_t]CeESLWB$ *lp3 f1*jec3B{ xT}+lD^ǎ4؟^.u9>Y HN `ck&S9Jɘ(ӆ9Hceײ!2}Z޺a!T |g# NSK|8Jx=k^X"~_YݬjLO(;a~t$x>1@*LBڎ2+kD?}rS<*8kGg; @#_8(o&%r) {GF HAR4Uc“Ñ-=ehA4QGMi.#r={x(yZn3ŒdxW5U×_QvQT* +{lQIJ,+D/mPxT kR]Zv{{#SQ|/ bpS'?_?Q %fG &t⏱}V Q Ks}Q JBYjU@JֻlUt5tJ@|4Z"ЃXՃ4n+r0̱G}6CE?)l\䧞h.\op"PU&%y4& )Z aX`D-k—^y;?B4ceJYh?!MP9sI^~HףF''׶52do]"EkGiA|v58ucޘ˵Du}έwrWDR!&%vSCdbwL+{-7<=xJb#@{Q&%p9X6RS) (y?K:n75eQ,K/wSVFfKNO(hW}F:>~'i;kAFZKw?MZJYeJ8}u %)n=gݼ.2M\YBg5q!nH n3" A.H3W.6xYPws^}XڜN24*bulCb>C\ǫ`),KDL|cVUZ_ЈP?wdNUK@'JN[p)vv ̫*S5^°Dԟy=Ȝ^+T5!!YQ\xFM8|8 PƠY}J"q^Q'<%'w5QUnSмD~%|@Kq,54rS8TlVn>+uiU{d7k[nK[|||fǘ ɖ Q 7$fW PmrqLqoI|^<ёl1_2$W~ Io=]mö͟Z nQfL3^*~7z)VV+I#qTG'ŸR6ճ){ihsf;А-FD0&6dgN27N!I_x $8$t&].@&[{J{dž}ժL8v?` )6+>{K,ZN{W [^Y]*mxU>R-G=mPm]ՁƟfzQB}14Y0YX&y*zH2cX'g%pp.k eAJ^?!lDZ^e‘ol(EEEˤ6~"QeqP{m'`=wԷȊ (|OE c_`C<?ù{[|*%,VճWN' ՛!j0/˒rv X <)ՋC(B{yDSyG Ԗi"\Įh1V@3p1 X2m'm |?l1fU\L'pY*PH^/RkW%MCBox&'ZΨveyԃ2Mp=xЫT$%VW,*q;yYpaⱿNbk{vtI?+ fpMydKWl{Ͷ'Ut< _e%Dl] ̊tzs/E@׾s£rႚlp 1\7KVGr~;=PK{) ADGZ5@ȕIq#r'oZ}vmi뢀le-\+-Š:i T$fOVNk2덽[cXwW_hVmARiu[fr]IҭXG#,ʼnr&=> n3ݔf1?&4l~̭Rv ~Ya6>4l먚JtLix$\oH lE4r#뚔7>N@^OܬޥИY, +o{P;{lz OE rjBdPR#LJIAZ; xVKԛ:l#`BmdR[7n=">:2YvzIoXÞI>ZYQ6%mT6!^ѕQXn|gE!菌DawZptWkHOE4.@᭣3\̕I\(xaUIn\PdU>GqJKo:6G<%K cA::ĸY\bKix+7QNJ%O$J bh .tfQDZϾ_ҡ(wMf*ִ9Ke8}* $B9/+LvL"&8σT7AգvJ+hǘ8Hu[6nT%5\FdU<~:9܇d-xXO/:mm Ta؉KRv;wKn/heOVM9-vTI|o|ι\Oa'z/YYuJIFߚ~I 6 XL1A]Ա)bI#|||&3O)XXJ;z~oYaM: .pbn9Ӓkew-$z J- 0|e65gsjFF\6gJzk6_*i&&ɨ6a ;멓r6"ϣ;,Fv"آkvxQ6aSf87,ox#*x 1qV@91B9Gs'ͬz;.)$im3O_zՔyv2`JRK(\l3ݱ}/Ύ`Pf@xj"'.A_ .kmJ`si$!񫽳;y< ic$&2ꘚ635*3rZr1ܲ@Fe`%Я魐2a -l6tfQ T:8wdjWwJac\DMZrU#yV>Dgͽ?,0(A*ԗȄ2d!4yÔG݈u"E[YU5Ih kTaVNh+Y_zܮGD&ro|qˤl!]sN[4}y/c1`:$3읆{1zfFrfMP^M(c>7Ow0Nα+$.p_)'V6_Y9|}8ϳSڵHݠjfXboH1MBeomS߅.dv؟rb8*O.ǂmbg# 4xhSM=Q7n!pbj]րiCyL%]#{//]|Bݛo#rj)ܯ<*?X.Q8lUwwOJ;$~ڢk5VdܣY҈_|9,}X͞qa6'e+n`6CPcZQ#O4ݗm:(,ʫu ЩDvNXt?mu:M#>Ō%0ҭRѭ#3 J+LIiKFF~zˑ>!* x?>غp;%HRVe߼:b'GȐM gTтn^0!]kklGzldV>+8YuV& 8â'tQypk],f"-nw7 RLJ:GVu7dEXۖ?浍GUS1o"HZ3a}Ig߄Te+(G/Z5t8*I%lŵ}=FlB R/\/3%QTJwx^x+- Pj)E}<ʞ8զxaKAtXZ? r3i*Ú=9ȊV^y?}2;(Cөy5+.& 3=3 mF*B)Խ Cv1.4}@CJPYLr.ЩȓtA3{?75+jXs:\En9q&~`&1 uX WUZç>+ [9vEg Zщ iԏZTE8m8Qxȫ!f8#b4;Ũ^PF,OGqd^_(j9'i^Rqy7yLm@#IMY3j(jޏ{A“Wgbԕ7wmŗa j JLV#Mf!A7@G`_C97'5VHt\.;0L{uKwY o\P ?VQ}> 2jbw>=R 4Ј7@G`2 c=/Â%CD V!4;BT֏G[fqt~V9^f߰1REhK+I ޝ 5C }E.)1IK3c랲z]74Ү &,3O =>wI/WP pH.|K'-Ճb3  br8Qjaxr2g΄Bl(T7lIΛ"W;3C,Qg;ue)Ce}T)]zB:$wQꇘc0Nn>Qu4ӵwA̦XǑV=kb"N{pAuv-Lֈ x[kS =Vpƅ+m,|Kl׼Gm6)G)?MǾVkW3 =~PRe))^{~Z5G]ȵL2ta%ػS4*݃-k˸\]]5Ô-A؏Ap `cnڥX} b<@\QG?_17"6l<_( 0( N3_j](t#jE/!`ůR?'Ẍ́^t~&ut )|/qPX䉭 =%3hUL?gߪ^O`_Hb>LOa=h;Cŵ5s't6ˆ\GV+!pH5]͕݄5RD,ϧb1HnmL'9B.cm&R k]Hh[VQ18sfL\蓧6۞łC# ZljeO#cS_Ҟ05~+6DS j+L0T'8:R4֊q-&7Q-!uQYn0<Sw *.9}5M b@k :8w}{7^5L&Rk#iM`0VmS< %A=,Mlɍc9BIЖ n{%*ن16h>λ j̳ϧi^"&-=CA6A@F%bVriqB4WpI~D-n!J]sgfВ.mV_ 5oe'j Lc1S^+broyBp2qwU )%.w2#@1Va<з9PA*:xipw\[bzi:#\<]X'^llωi@wdrtSN񱈩ք]?CA-Q lTUhPn`$2JHʍNl@܀uuҚa_^)|7B]uk"+ +b5x93)1sh;gv:L/U@޷Q4 oua12ȣ!t^YusN-N+X!'L1 sK_ٺeEVPbEkQ SW1w,T2;U!8{;e RCHC,<͹ βT ӷǸNMDg|}'qykm$jC=!$hT#䣸?vAS@#⥙#[=l: :h d7فWbi!j:&dmW8Co9+3N)7ƮW[&-N;b1 ̻(32mb7Sәa@TWtH*] /]U7/;޷U dYhОrT=a*]K|MŅ#`F̳hnn3rCðݰ8/2ʠr*Kw.(&; VpE'[]pS8&¦ 5P|}gcO+>.`'MؙRL`컉_%q$E3ꀊExZoˆ̧,rJzOc% "P燄} Oc,F-/sU5% BŁk٪XDvo$c^`?1*Q/-LqD]./\8NqSx.y~yuȾ̷ZxN nn*Ks›d}EY0+YtǢ߷evYt?Kǝ%0ݐ%GAi_eˁY.;OlxW#,&C;͕뎾j#MܹotT uq_>5a&g~^@d԰Ra0ۭ!9ZĆ3:9(^2BdXbݍƣC܎bnq g1+OƿLSRo`!|_'^ե$;=cUa03(d/ndU!ZO,0d< 9KMqZ|eLۊ֋Isx0N?[١qwi&f?|łҞ8Hƺ|pŶ ~yrR+sآ]MqѴvQP)- Hj/'}rښ0%Ic<H!|Ejg~&b+O_3 2dug%0<:#LYObyIP Mm/iOY֎kHBmaY&q zR#1a(E&*0YDhT (U V*՞ݿwh:vXr' K'%ɚ-?:jE"@Qv qS|'-!ĕx'dv ~ٚgivf̕?tHT&]/R ܀G 5@ҖLC7=ؤiF8L9E "s „eGZV9#xdw`9DJ;&ņ.ӾZ[H2*/+BSoVpt㍰)i]n/P7#eP4rhFq,j#v} yzY# \CGwMK[#R!kP3|URah ӽFRT^ S]qa=ΟZU6r9B3HhSz4mGOh0Ld/  ht4+)>!1i^TփimdrB}s}d_ h19c2Sou^׶ O&G9)ⵅ/oM\!=o}"u8֙xoɋ޼FWfJt OaV-e2*O#TWф#D4 ʹ =[_&?L*F) 2nm͈ϙ*ϓ )r0K ;v.;:.éFd㏈zbt9( d}RhJ_pR%yGvy<y>N`2 *T}N]=6dOԂ&=0#~v2pH|!HBy{sX\aբ@ӭ@*Ar1yQg.ʑ^q`^]ڼƬj \ s:4 u Fzܞ3䘶nұAxIs [`sED[g#RCȠy GAs@oǡhw3 R7lJ鏙n r=ޝ89w$k A.7 Vc-ʭWȂ7H!0~6z{ JqQ1pce͉{zdϽULi$ ó J ئ5{مf[N82ąr| {v?GYcG) F=DR۫0}A,e!H?^>?fgaz'19l(O C&CX~>QQX67cq:tb ;/s`:xr"^6$z9B~0޽t 5J̾]{^ ϞLeY;xkg)]mW?Y6|Lz V?kv!j}sUܢB16KtHVrў@;B**-rw2DU}xI/ưO;}gU1ZwAɭ!oXz4mZYX-"?G킖 b2`Rz"d2r s+8 S1P/2b r#YZ_]uΦƻ`vZQy~(' s73/2eUfpxʩ9} X;k,Jk|μ=a\[>jQB)k\NC\{@hn2qᯈ|!hlpc&je~L" Mz/uN>|$|&ϑ[ь8Su/IXݚɛ>AOH(>$AM`1O!R+} ఽrS%}KsS4,:=\k9Zv~wFR_neoX!)|kS J"B߇s4q-B4DRm>k~n`v75\: x|’`e͹zo5(olvQv딁ī$bPT rK XWxMM,Bch|~d !j0,]}E&5+yzɃ6\2:1d3ݩk ˽k6f[N6G]D0L p̲Oշl$g'y)ZeyN)jWIʥ&bOLW wgh*Cxh[[_v5ܸ59[dK[%k'pZ_Y2rt\5^At:팼$#dCt[=F]Rn(q(M󋋛hLXz &DJ+CHRa h:^NmUW"oZVE!!(UNDBTrp׆ >DYFO} q7s7 _T8iSdX]e 8RL$Kdگa%d.T~ɱ^iiD'i ?&e$JݧRY lj`Ǒ0cPk3G-HΟnC e$،O.8wZ ctFn0^_J|tɩ}*1$KԷB8kIL.׺nm3"n!lR|]lHNENK[?j/r-N& m8aSؾb<C/W;?'6acA~O`x\{ېތ:,r?W3-]՗w &@xQ(7|9Gx1f+{G]ǣ.n\*.Fn{;y0v֛wz/J5|5/O1@^lǕHUSꐶvdX Y Ώq2 v˚; ! 'S> q.<[8QEAzpq5PBZN+тZ)jjVy;h$<Fqģ=œz8Pwrbo}{kh;}[jXQ)O:W.uupМLMUnS֓XYbJ6D I=k8;.q L@{ zQdRaen? Gz_%08;uKq:qSE>De3*+1 D83[A&5ɒK}ϼU}pK"ɵlSժ endstream endobj 1202 0 obj << /Length1 1630 /Length2 21386 /Length3 0 /Length 22238 /Filter /FlateDecode >> stream xڬctem&vvlbNmN*v**NŨخ}q?c5 QRe6I]X]Uv @93 WB h/fb if 33Xxxx(@O'+ K& J1{@ff t3wvT53X̭lJ jIu-@ gebflF0:l}M)͙ K`pv03r301sGEp0srvzX9,]zXٛغ_v_/0% +Ŀt4r'՗44Sҿt_0_Z#+{g?VF_f@p20r25sv;Y'_Y\lXXb|ŶcgV́M]CfQ334_Im=fpL @;Hoz//_%\mm;d_{ g9|l=O^Z]"loE 3#VVfJV.&s#ۯKnojdkeo ``af/:5K+HVE׿gҒR,*} Wny<#"x3prXY޿xX}7!gy#'+YUIۛMU#{ӯi&NN_$k|Uͽ "Є7:#;ӥ;oxB{/ pCYZqa@ ?#b6iG‰ H/-UOE/M_!e'A~bf'΄~T'#M[aIzC=߻es! N7ɖx6kK:)ScO9}G7jj}]!ŹK xdڥtT/HC AC7``=тmvD|7ox*7^{\}Kr_owp؃qs^Hϫ$&ZxqmN֞-uhtXvYKK,61!LEWVnsP|QQhTKUi K5C +7w@ypv`/TbQi! k>UCFY* A+A՞?$)2W?_۳9{Xa{įYY )0 TC⃎*˅ ٭[әWhEЗfĪdko-[_nJoL6=1E+GI3lH7·ԨQ DdL.aI.kVq/c*Uj]%]4pי emķA. l4Қ~o9m69oň 0MWo:\\ Gb\,it̆:[ vxߖ%ؕ,BT,!Aɦ'|%Ç"P-&gah24AO5xJU;eZ{p:bsر/s6v8JB5\?R ZN PRj D,w*Y6dIH)Oy9KQ?wb}{5Г5k7gU}uUbHErύ9!>O@I+R(ooZ mCFj7o{# vHR;%N3~uLNpHgR&hw0hCW'yD[$wy =2ƾ Gæp$ ?(tIE3Z9)oCoi/|&e{^\J: |ˎp։y㤮B%75E÷j7PfDZ9ˉy9K}*݊˘@ySgxx0?)nԈBSu:??JMmP)hbSŃ1vO,?Iޢo6 q<fi>.]1R'YRGF]bE#\ATq;B6CF/{"Ll f>143o-p%'_G -2 QCe[~z>\EdM/(C^3Y*?yAnyT( k;/>}< ;0!<>֫wdM>R3wڈ(:a)؞:jy۝Iq93"7%Rw@{Bo|Gꘑ2?kdSז T W[4sCjA_G2,M mk\biVe6v_O7mE!R"rTUlvk[D^&ɰx6?[$9g}(,MA)l,Ւ@$TUY;feZI Qtգ1uSB^Lui,J0i+h!2Vw%K/ 0; ^~\ka n8FBAC ݗ(ђ.tO/YPcnA4HtR-F[ziZA4udv yIwO$ڸXw6sMfI_2"~9 'EoxX~&Y+E/: gZ{E"+Ek X7.N䥁}KcPT:WAaCBc[lk朸:ЬYF*3&ņC W'YQ +u,Y'p^5)Gs.p^S\vu mo"Pd^W4A "mOur=H8>P niɉCyFH(hl~G'w+NȊ 頗"0jPR9ݦHc1r+׼ I˷cKBKq-oRKAJA-J{%^ l&vdV:)ѝT;p<ŷC9W# .떕qHX]ߓ+QoVl "2dbYpp= B7f P"zo%RE'DG_@ ;;O$q조n' '| :|-k q謈i8(R;2t{`Dxh|;%ag7MN%m1YAPB)(>}Qx1kWF:x{L_{S1Il49#~Ԉ|M[nUpS+#"/emy,:d#Ib 6X *l^7ܖQLY0x==koy l߿5PP' +<5'B)E9cMBYҫi AӯE{%;t"%^';%I^(ՇcDޡx#rE\ D"nDMb i S#E)_n {m0 ܣ\ '&Бl}xrnkޥ=, ߱RTcQ/HMDnId]s7tUy Ѥa#>A\JxTN:v|siv;>gc)07eC?ӣ*Z˜#8K '28@ D+F>t۰l=Z2K.6Y jCfЛx iU\+TckKr}N]'߲0!ʾ0L ]Ig[嵡ÖRx_^ QRA=}6+$`] (>ȝd5#v=*fqE.[P~[K>ZleanT$ 8yǬ-t!UATb * BPefր]Y{ gg9?|L!QDSQSW' `^}|3yousr4d EἜԷQFN9H5+e#yxjde;5Υ]>xug >6H J>`.~C{bhw8(̛`olH]V":W][̮+ŵ|k̤nZu}VVJVVf5 GfcJKNuOX+F!iMRRz\L_C ]^J@IB#"s%uYw0f7~L%hLgnj>ò^rfMNU{q> zk$蹧$!Ӗs`(ok8I k%zgeZH%ֳ!^bV`Dƪԉ0 B!وn-Į(+C^j7Vm'; 'TrbsaM[Oz<.lM1*vIP] >Ū ! PaEN9 + o7NO^  /;+AMBFD+q lm=E˾򁋻?" ԙ/AzHՀߙ!BL/J04nIF:#GK@)8cݍW4"$_&$C斮D>J;oS>_Kjזl? gM-NYvFxj!JSzQs bcx|$EHl~5GLY߅gX;ɍ)}7DoڄGc9EBN)QaGz`׽Of;Xq8)$CN9sFrl~ꃻ2;w}NB=6,'#D|r](RI]ϙFo5R06.RI#&#K< =hxLMk,} {HEQE;ϭ\(V,:c#WJ[b҄Lb+طo.B* a,y)[aU$Z贘^7|~ۢbf6.q{0idӴ~ŽIb+1ߏT.31XTnM -#XC/@ˢP[FSR)5!ޚ{{aȱR1 s,'Ctp(, mzApR)RT3.eo283eЇzRw;ߤI+XoCB@xMqdYPעYk+(F+ڕf*6úa6x]ycdƇyuxZ;&h[ql&3xȽ> LO"l1O) fa-L5y6|DJG٣XA@8wˉmROq+j|i7,O? o, O4NYiqvZc'!,dHl*ٶSuOLP|kw{;qQVQԮʏ[,`z[Լnh{}q8Aw]?Һzjx}սNjNٹS=rY{@HۡljUTnvh@8L(zvsie wТ!m'zd\4;GzG,P*TtBmruO˚xZveF@' F$((J@)KݔV^NAtY/db:%br0e#UažK;`cÂv}4(L. @#^G\ŠVIgҧVu_An{hv]Xo#Z~{vf[jxex"Ӻ{ۆsn_-KFU(J eiYBSQ*sh@%Kn;٧}r4>)K#rgU/HI;YbF5ke\)㹻[Yx{`.9c{!ĕ긥Jsa~cd[.*u"|K#{EN8MvǝeצN] K^$8( D^g7㡼=nUڑ뱲OeadM$jFi0UVhRM4?Hwڢ3]Ʉl0(_u% w9.X%" nT+҉vbtcN;j-sʼ`AmVђ"rMҬ1s2̃k A~Bz'1G5Ŀp}ț?R"~A5r=[h<#C0,% ӕV>tRIHMQO˨qO|3T]Oڽgg%iLjhڬ*D̮6KJ:9)-z /9)N Noy&l,v;D3zS2 8ue_:I\ @>@I0d"sr埦|Dhװ>8`=wS7HF˵[Cw64Eb1+ Qg{,c*{lv8 L9 cw?K9 3+yL d`hO5Yo9O$xeZ[Gsϴ. |eעrU).ԩL;S&O[dJX X6 C+GyK07# >1׾,EU QB5,L_b3`/&%Jq Fnra̞l)TK(j _ av@aoҿ {6QZq̽xVoݱgJBuͻKWpm|?};9S ĕUПo_)rs8?JI]ZWG {ѥFyD[" T.sd6)r˻~8[s} C Sl/}hOc#f"fR:]^%Zꊖ&D&;"[77觑/ *2m`MRgR4uFV,$JF/aG0|r6m◛qP !d|v c820))0Lޅ%f{n\A|LWljߒvUe>pχÀ#dBy71Hdkm;)Jp?| {fWAI`?/d`k">4:["c Kqe+?@vm>! 3 8Md0 Md&``NmF}/~vt_^"Ujq37ukhe$&CW"Ydnbܓcg-vXo,)7 0a,  m7'|KܩJF]"Cӫ F~dcSίkƧ{h&;]C.],d  bQUBt:e&1؈ 4npmbQs{_`QJJbԟ^( Zfr]y^¡xY>Ȝ寵 X.(ϱs#0vfetҳe5ZY764)ose1,ΪzEkR@`j3UO8XH;l1vS@aIN-Lm >ChHg әkLΆlIJCK]]Rs.8}0-|cv]tX ҵr]x# LY?ӵKgF YJLFpdjIlBҹڐ<}]~$:[/=S3㹪gs| B?]īly|ئ0 D ]UlmMBFFKĕFPH b%VLl~t|]* iV~EJ)j_ZEAy ˔ҡ=Q7rQ*tqD;KZy )X.$"QQ s&.Od7â` Kg*eF)+KӃ4GawyuÍE2J נ㊢pJ "}/wd!d@041^P~e[џ3Rz9!@5)r !ƴtWqHa8hJvbs$dEg{ıORT$6.}WWr:Н7÷nm2-<|^k I}帧γCkkA&CK0[~a,B_Ft^QSctA>u|RDuqt黨e[N^DՊ5^|5ّ~RKKlxkrN$ C?+=8h c@?fLd'+uܡc' x:F` wcfHt<\ zVO*$)ڿfuAXb^Un 6b>eD \fIw6T E"&py}lZe2X`5;VhW(RXMq% ?%OV.$!ך>a.xR?izDk=ѩ6MngDūG%\EjWo٨m~W,[n/00D Ci!RK<݆{xξ5WGSX$F&&[bRTqc3v48&g~`cw+oF-JS_ EEֈ7EU Yz `>9CkV<+i›]籁tSCc~i?c{7CF𗼯J޲["=;BgM߼iRf.$ hJ46d9wE{Y;P߉zYb) .}>gyW4{%;A)̀2VcʪKŻah}W K~a9{A^^S%?($ů9~BD +(SJX8ZK#2(fc&$Fj=5n7^I6>> "lй^Ž=-L)]}纰̞f/x5ؓvVŋ@\d{j Hf0L[(pxC6&~/䳌yjsm5O~YOKxNMFMk&I?/[*z{z\ *ZM|9k%jLra xR8\C#aMNi. =^L&/&T+`{ꊢOѹJ;3 Ld(5Fx= |_a SO2UX7 [Tc>jBjy}U5L$;,]Ȍ.xH~{8FVid"$i(6yiKrTBf "x靖lWf OXlAˡ9@|{ dF:cn~\anoYbEh?0K HaR]Ά[ ޱ %D3 (Ky"LW[\M^Zߵ;NN%Ƣr,929SYzX1qIpO}Tx@}dCIDzÏ $BF%7Vm>;0Wʤ)~ }˰&okYol9 5{>ߨPD^-|MXiz#:fsEiǫƫ&*FD5i-"0Z]iBWEC=pC!ܜ([!h- epm P&:ri҉">c,t|nLӋUϖ +A#OZ/@ ;¯o8౅o rdZ[O)/cHɑ9L}HRNlkV&%u(/4DŽ-3j1*'|vPLNxJ7B+$ 0 g-C.Jp [;>H~ƴ]֗ <)?'NFiu/:^+)k-Bt ぉfӑt9?{\|]/u!JsM@ E5*yCuN[cudãʞ:V4tbU~4Pt"kjxYK Zq1lFy~nk8ǽϔ<(h/&2 }̣\7fEg#PͭغЊ>wgݬ88嚄u=zMcPxX 4MCv<+AT}dezZ(1Z>rD}|rCpR.޹GIQIX5^`߲䰣e |iQsC2^E]R3Aɑ[/KzlNVػ[m؇4?Dzi'$I6z.^nM\L4nޮw/NE}6ZAujX_rs>FS~!>n R|^l<bܲ×G4˷$<ގws>NLe4j;lڌiKS"te j1]@R"k PYS%ZR7NE=a'űuq(Fv3~ٶ^1̌R;"q"+96y?a;K3?/mM *^a~~xgϹeb1>Jd924F AZ4m RKꣴ6maag6}s~??FopCR-aufuKF昘D <,#Eߞ<W/l(%ev=kR's[ rـo:9 쵿d/0$ Cr.xP]tKkě ZАrwNs{ ꤣEa Mr2uBY]]Bi&݊%/]9/BHxydaJvfHMoHb*x.cU5IG*y=>Y!:)\I-PL0Bw-84u OrKWrX2.wU{"ԺJ$w S{a8@Ew: qL&OIF"j$S`kG,?J_Q{DJOǽQUjIoO#c_UzB#Z6 ˏ/kNǽpn) ǹרB60?ZvrxgUᆚYlqbOX¾r£+ے>{'RB$sg_fI J7 =)ϸ0S EpCWwV6|"ĴY JЙ3y*NR"tw͎l -9b̑jgܛoי/&{3"ʏq%gF L';H!ۿ{Be (5sM-!i U|L}^'l6]ĺa(vqs ɅѤa=̳ѭCi[x)Ub_M؅n_v!4L{?{޺Yj;-pK`.<}d)þF5w}[ g}5˩#s9|uS,;aSu.MvPH\ ݞ\0K l4bRzrex*[U,F԰!+izDckjWjR̷^3\-3"!EI#]kXXNhU;m@`^@<"hOIπ NyrC"xhl A/%fz]{@bYfoƓzzb?"/dvApp &=MO7q#dZd`QߤzpMbY]5XMZIz5- Z9][I~Z^%JO[l(&7$S)Ny|XRmxgX[Cyf&.zwm8cD`A+")Gllux~a7Dz >ޟg@w餧Mh;x m/~);c~^>naedƈ"%y͉_$ zp6)hTUC|u ~'N50*s`Nexy?er7-?.Hc@]1eL pkh`,w^G% >@Y뗄x;evp;EҙMQ |κ/N%*NikB#oNvar3N1aYnp l.H,YMHs/l#@s gRRz~킦yR0(ƐM*@mXoabp!%_5rsN0;[riU\Dk~8aebLaGܒߣ,>xA"__h}T5W63QEl lr$7!>Lk2,AާGQ2q_7 =A먂ʚ~U':Z;5>V E*[׏N 'e#M<ދHNnÙdoLyxR!WAdD.-H)t9+m"?[g 'r+ZӸJN%bJJ2KK(w,.J2qa [[s:xTNaNlUr `QoBiXbaAJJ6kV4<ՃU?-YhpA)ԁP 3)H Jn>^MPf?R DEU(|lHٸ?/FVC`qfyviϦRVRӹbO‡xS3j* zI|F ]VO:ſ(㯯@c=3-c= kgOPӂZT؜S,^}ё8䱸h ^;0Xû V(*X˖~pFр+N<iT~ڕ) Aqnuv'ˈ2ɄØhyމ$XSq*\_ fڸu[<$xf/^x` 0M(,--`\hya\9/ UTGW;]~De`ؒFH+BQ,*|k!A-*Ѿ gJGnA!C%C!LcZSV!n4h P6'\ \z]m5BglJf Sq"b f\و^KJg=7'EN,g?lw5*W~T-e= U Wg/[MpMfR'2Cy]+ _~.IܤL|tB5'rg$|c5Tժhdr%ϥA,l7uIn eZWhֱ//nrK~^OԵNC)[#8֗Uи|rPZi?Bƅ9t0{7˖L$ޗu80cx ٵt$ TwN@L:SE_I0?!m*TQ\b(PvkCL mw4W_"bU-Ki",6=_ķUD\JW1it]%`w]Y qbr*(z2]Ӏ`eJ]A.0j^$K*g,iwqzʆ! P6;+nu`(gL&>5t7j%v]|_yvNbz=q"^i6ki2@RK!6m_c,,E(Fhjp,NlҔe_ׯ^!O$4JWZbb p K.zd;K7tFiF )?8'{Jq]yV43Uy"2 .HgIKuT񡚩?p,aof0zBp|yj_ _&U}n Jh$fb}M>8+̉ԁfcVg:a =o9M5Cz=FpľJzf :q2T_bbE+ܹ|m,ܼ-q89\ݰF=o; )i{dK IYY0f|ɦ j0W~Az V02~[u\dX nT(4ǎ˥>O2aC>_\ʆ-=Cg96#_@mv۪}%a0@,XBh+CpKDw_]&kqVzg|xX{;Eq*\u9ۿNhBsv$aX e}v^|J:YP*AkOV#I>iWy D`NI%6rO÷$SN5NPЫx'A 36=kdZ8yD4|^q|_IKG=w:c8#<9>E^с+E?N t WE-TM8c?#|ДNEkυ-𬱤 &d=eLwڗ̨ txUBo8Se`nG@gz8LB] o#5r@Z@y#v?2dGRx ";-(wHZ/G]كIQ| X9ӻ YGuzUDq5/NO٥IGY?kL01F $)R׉X* uڥxULMժ7ftDOm s#p@g1hڵ_?;if2d ;BΤm[랁ڥŬ7aRF_YR)+,SE:(A|3i(_!al' UբOC}kQ*)&famp6"Q6€;ĤD2:Ԙ}=rI͔D%p]ܒ%$VNfv赊K40R\+̈́ܐ'Kʇ#s$D;R$E#}XXV`в(j ^Qi$~5 {>ېCjHꃊnBW^pE vN!Γ2J@ۜZA}Eo*7!Y~I.('NOЕ,~J0;QlOjkY-&PPE'i 92}|ab,ɺĒ}[H[^SL|BUkU t= '͆NwJ!1?j^6ɝ5k0U(> stream xڬcxm&Nb[;tl;+m۶m۶NǶ}gϞc?YUYUZdDrtF6@1kG:&zF. IQWGF&lw4wrTF! Gu731uP*+Rhz:X~8-ml֎!@)`lf ʩKPQZ!d`if23Z;66Ff@KpumQlVff{}kǿwh06t2'rc%dko/#oT9hOlj_K#CJ/_O, Ro`fJ?3M,abs;Y'^_6_99:-ᘘ4t~601[nd:g.򟞡h c7$e>(o!7r+Gsѷ3Fw,K}{? oefr֪g?1!M2DFo+H`o%W6[Y_tJfoU &)Sme,+lIUqx1s9qdf / 3(Q 9r\/bps$مS>TvC-$pu]]Њ*O+~>n3hnY W *7N5 Gmo[#^0V8Q䃥قɺ1E,Wcm$!sȣaj ]]J|Š@'ALî!z9rHt,uGT1m5"2W uoB/vs\& l}\vpjIq]]-" XV5=]D&#w%R.EiwZMߤIz9a#r:9`kneR*I *<),:0TC׍홿OO@E`rLHʯ RN(3#w%,8pS'r/x&w.`~dY-_.C/'0-ݟdWC,5܀_BȄLrx̅٬/48|q eaXib <њY`v۽uЎ9-ǔbcw:!"m^rd. lkw6kO}wZIOG)ݥTtv/2 ^I3cL't>)[J]-lFIDʐ΅v͍S*{\֧zb^1+9l^ pSs7뛾y&G85u u.JGL #5sLɅx%~0}*;5MO~baF0\!#报gv^hGiDtW2_>Us ; x6&>P%.鬨蔙S7qEtaNzซV)Hm] Ңr<[|^H;V#<}эoA/U+gg0ʻg69IO *0WfĴ\ %_ݦBnJыgC JaEfJpPUXbg$ ueWYϟ}ҝ. P&ΧN?Ǥ&vd̢@զ!橡b~E4[¼/Q$ȗ$Db}΋SbD\iߘJ($75Q跇$%Ӕkޖh3_M*d(Umo} F*lZOiݤ'%-D".iq62/7&;  yx -Z%zI:w٥*ep&b &Rb"S(:F3vr);7M>^ڋ 6*;Ś'f+#DK3?FV \F?~Gp.Y(isB}N)_njM% z0Đ'olxlp##7Bʫƃ;EW˓].88$_cmkD> ,z]AhAp&Μ{Od)F!9?h'ܷDR/Qkz MrhPgӮVS'hJ*H!pBSӇőU@WaVf30\Ƣ?&:!4.gZz娗g$UƠ &z)Pa2NweS?27u7y>,&cۥ6\M X@MT\\RYB h%. 5>S_~k.(יKɔ3f$irr7şpJ8S:!x~#;Ř 8w< %޾%e"0jD]풷9I`59U&s.N6)ͤef{./X0BCZn0d/#Gڼ{K6BKybG7LwߗӡSH鶨ezĂ%Rp*/rw"I:b5`9TM ŞoO'װ:TA9# a&ȴ\DĤelvIϏ;1{ ~KỈr^IR!@9i3rLfr9_6N#bՊi!E8+2[hwd,"c7\5&fiGԪL͵$DD7'`a%t $z .r9~k0|W{2:)Xq-:mF?T䴊rŸ喱\,sSx@*M{w&f fWEKbddSGȍ"wL Wݖ?uHGVm;qL^) aD PرِeAgK&,k]dJbU*:bsF^5,ۿ6"f!y. VDz(8 gq&cuXO/cRpsM*)|cA $, ѩ7^h-4%ZX;{:6mdD%N2 ]i lz"Bg^ J*xzUúfo1B5zECoSwY:!־ .يU]*V!P N=hV`6rX֫7w}GVq0s)qwDF\-gШLFʿe% tf1ʩ t_KRj +(>afe,՚L{.p.;q'b@[2;{,?w旦~-q %W3Ûl"5Q́B%E0^ΏeCX!q*j=#~iFs~i a+8l@1$n`sTq*]EimԼ`茭E@i.wY 9{-'@[hKsPͫOV]Zvac^Yt)ngiݽ@Kk8|ߤylBe!V,u5Fޚ}*V Fuq p%#jSœr/"[F0- 7+tL;huv*m`zUƦyH$& Բ+2 td.N9#E1fA 8BEڋ߲lm#<ݟB?8*7Pai#2wq%3&GEfqSzkpDҸF55ӅwBP^}c@6L+%uVU4d xt&x|lV 6(w3ᒥ#xmNE<=s 9j)&!~^ɶhBk ޺⭑6X+ODsl0 ?"9 HI_"^S{BYF{k0E**c97 {:q|5)tD!8̛Ǐ9m_A'[-٬wtQ.0~i=QvrZ'עuM[@= O? h?IxSgw#zj/k[4!qXKiBy!zoz2JyA*#~yl)Xp׭`vBK~2xʙ8\pA` _W$ϡa֐Կ. V27'eIfõTTsm0kxQ]X8X$[mk& =YR3 >7 0Tn؇Fֱz|&#AD+M.?⫽!<0 1ySAE R*FP=1^2j!l 5߹@,p4GU؄sOr .zq=c38Nhϐ6kጧlN`atAyxfj$eiUuՙv 'HڇO^GDBN-ӬC#ޟ+U0!ȴ8̈ZoE|(|?/' ':qv PV'm;IlCo.!u@cԆKws90ӲĖy.g$5 $:CߺlbN~JCɐl~I&g=rl^pbkeVڶ$oKH ,w "9@I ׹uqYr(rP`gJ_{^<~ =1&1Dz/ܴ{EEվW6]FS[;q@/˃s ,aV ?P詓VB=H|ՇD7u2Ba)/HyH^X R͸`)aQN#^BC7Ńo>OuZ3ձTx{ޘOWmo"t0K ξ'O۔'bGtbۓ-6q(be"U{ y@^Uz@$C {pL "鱾ʆ,j"xD# X0j*m"ͥɮ3UJls<0׸sxsi̕G&{NDDNF|/;]ĸt}f)F( wh$ ='u=9qis鐓'dSZ}!(l`]Uj,,$Xsc$(4*TRXGJ)+r\+x_ZUr$J,fYQd>@#O[^ZbXfy䔁$cȗnj3^3yKFUr~*i(AQCK~ɹN,ݿ n8X-Sjo$#=t;^~sKJq8ofVWGs~hڒn R$8Q l%F'Z":@i2i7F(Qij5SmA-HCjZQuT?L!Lx|%a7z!-.̻h~F1IP38`UIDxH9R-$]I!:UpWpY7G٘h^I["gO asV&Ճl jfV{0U1>tƉz __evUI$L ڑU==[WKCon?DY+ ]U^lh'Kпiϴ#lʛQdWcjγZvN$-k_n3Ar./pN,`0'At6AvQI2:NcGwiGa.4<%Q*)b7/M=(8X6rkH|nw-#VUM;pu);ϴiuVmD#ԞZrE{mIN["i2PX:\+\HYtMIKNq r,[yƾ Bt[M f@ڑU)C + N!UJ(ዿZQUIV"Iel-z'y"O90AZzݯo/dɃI"[C̋T6as)h}{hĽggvW4rŅޘX:Gk9l:OW~3ܜ=#sq#Mn@Ox˖W*n ×6.oaS)E6ګLȖؼj nm|=n``*l.QHw/[(sa|oznxmM{bB"~ P lpclC8x2eе&iSn} uN4}aZSݦ6n.c 9b1>j86(c_F9|csm浜[lGJĈ2<`KrE,C=4@z5"KOby,H aG8_,W5kpA5xg(f .Nub9X{qq~@o;n/?^lTa=xYVVv e1̿˵H³"c`4WAJ#!=+/ZLݽE {K@Xo3x95s#L^ P2Tna-TX.A:0K#]PZp,n#q˽b-l)^gG<} U9;-Iی*WV$ -C{%!՟H].w?򭷖V1I|>kE01݅|Ar[Ma8L3qdt*DqÛXLT9j1JNis$ ><_ -LJ!yR]1aU8_֢\S=V=ppBV:p=HǩAh[;]Bqѿaqfp[wL}dwgwi38>㙃twTv+}[g1 lkFe:Ѕ\OA_`gPȐ"kF{UUk$ y5Ssʖ"K7"AMJ>κ/# Ы'PaD#QrIemвs ʸ8o)^yGrɫ uvcU`UtƆR~~Z v̯2]YEQ6eVr78/- 2fEE+T7̡ l|~2K>Ul!զ/~˺@H?G4?]j01XQ}Ȣ)~xt*_Kc>QqO?x-9c1!w'( 5x?w'AWx`+W9Ⱥ"wܱjXvJkQ^ =&^>Y/' #109()Cg?8d8G{HR8X<& cG*l@306@!0]\=t eq^@Z(0"MȝVKCOnƽite:2y8G+}<%Cs&xe|&6Ċo3{ ,M_HDtu"@G%Lg0~ؗzJg||i S.rޅ'hX-zw\f ]=XN^,f~=T(JeJc$FB BF'1r/INbL&٫SG[8͊i$0*A S#1&C;!jC?N(ٔt~Gs%-i~2t*MDgFu62C/ʇkw;%k^4qp^095"dkf^'*W P{c*q\_a)W6e"o>}9I + h2dr¼YlstSkr/2.oI#:h$sitUԪ%`JOx^ZzvQ˫_D=ϙ#R/VQw<#(VoH`MFܲϙ2_Ǚ" T4녬7o(RQ`bRHz{ &^XY@Az*~o7CRd]OF]jn*$DŽ&Ȁ.e`ZUxEt]+|-| *S8Q@|J9{iM8y-0ɹ7sU=L͉J).9v>TwɋKN.h$9MJQE!'rdziO9%e'jZb 'KCmY pۯ M.pdwzn 7tD 0(xb5{ȱlՙq 9ߺd}a<6 (iQ[zCҙcɖ68U^"S;IS֪#aVoJ[\ Jia:D ?tw .Ö"~7xѯDS Y%p<@yg P Z#[v,mf-JE''Tz`Yd%U\7{oA" "Zʼu൵+9fHE&!"3e`9d3zh;ɇB!$(mtv*),9c~$8B5v>/ҢxxEfwQ:DS-ʞQD,dPA< _%.kv"EQM@%p:u{RLn"'td*]]fLvuz26s}6X>cwU[ի5[_ڈHbV$aHKoP RUoDE JClyme"+@HHjPo2|%Orb1Dqcn3ex҇Y^ז:4kEcͻ+p;SOGڵ `hAVhIA0!_f6<$%jY9BD q[S,a@-LQᐛ% A'8`XV#sItћGUNpI"νafMJZ7P$Qh[rnSmeLoS.#+F°C, 0TNGn$JZA YDC*GȨ4r1,FC˗k&3C>i8G34cyt­XNOo=v/5J+D?t-[Gi[XZ*TG1{jY4%y~hkdܩ$B䡎 4 +T12Ӟďei'a% yKvZo YmGGdũnK8x1[4j篟-M T1Ь'Oy}aNֹCr)TN(v91h۽gւ"h8ExLJ+z m@G$f#?ftd;^i[ t#tCDAi~j {PQx3 1@ïS?:.ӑմ҂6x%}C2ӏix t|Gh-(N6O[D/GS3(8MpG01>C1' #Xo'3EFi}VˌRQxƎ>03QݸE8 IxƏ%D'l^J?`n((k)ե$"p'n./_|E/Ǝ LҲ0w?/]Iq4ӛd2^̈ *`QE1[>9\u7Ba8(Cmk J\%Wt|Ple\k PJ tu檨IiN.h@#7Yn R1?)l]7(:i`Ą_+ WvbQdﻺ95Ndep>,?}ƖaiLu(pcw٦ d 1UO:J*\%;y+)P:*)ĭQ}/xvhz|L5ANr$JmO*P ` š%L\YvjWלAOz?S ^o16z#7ՇbL f4b${/6՘0MG}Gp!6OKT5KeQo`p40ct#[[}B>uȤ 0k6$Ra|G'f@-W Y;ʏ N{e|Fb9\lnQVx%H \%lIi m[f],Z ̎ r5D[ ]هuxՇ-[5̉e; NEԏzxXJ#*?Gy6uOwh?@h/Gh~DtXYm?_\[|ŸTe-!xks`)2蛆91^iCrS`e`r(#=} e{>c$`!R9 j \c>N]?4G,̞l=؃+&dl/k-J5C2XfҸ چHi#+9~zx57dQ T{lR%qbjÞO*oH㴜 $^za8(YRLMXOst( AA_CB="? J7DSroR_M9Nt!MHJdʥ.ɒi͚G:>5quuu&=Q9QW9րU`ٽ^'fhd{17ͻnA Ʃ+='* h)Tٛ;"?vkG~Ө(=amCOtZjCd"yE^n'R [jSϲ`ckZÁ~|&8-78S㰩V5wB.C|s8۩K_y[\vYw "ΩQ, 5dx(RKq<[?`(yYM(xs:)sg.G*V5٩%a[^thQڝoWmw'w=i cy)irdԫ赟6Jԓ:عQT`24/ Sj E7ǍxuFS $;eKk#>!׺oA@}8i^;L]"^ m̺NWyo{;NZO{=ީev)O wGo~I%ASyeBp֟D> stream x\YsF~ׯLMoUXq#%9.?Pl"J;nݺI F>}, PQCTFj:A Wy᫠b GrE-tURKVB[~B Sv%pN 8K#%I*4H `FDĠmPRD{YWEZq -Ez<*Pn^G5*+:jk8ѭw@}sR.:tKfcYj⏎;x][ sz]|`dYGЉtvxdMn+Nd('=!ezH枭wg4,ZtVJӀ vҪYʂDr.٩jBn>.Vw^҃1mwze][tuk"K|Цf>Yy=/'O~<O^znp<4t8>ɣ\P:P  hF>ҫ#Fo|1ǓOwpl\?byVD>f76ͦ|N'>0\=rxsm # b)\5_ք/?fj:yt|UGZnVn:Yb6cdWϚ-v3a?W'O3w/^>t\B\5zDԋsUsa.ݵuic=aO,G[?+X'_v|9FlxsڋAͿ${_ _/3v1 hJ6fF;P2^] x "N9:FdMn. ӷQEZ.#Ěbnf?m\{g?ds?]GqgEC2yj/-qE]\Ey߇.oO^{r6(#E%sE)e,F)}Z,͟03j_暮e= *u^}ٻuwёW3uW;v^hӒL}F%-m}_N^+zOE{$ŚbUl*_ߕYDn##FD:bhJ {gN^OGhX8hT0].NVl*fm} w\km2WɿOޞfȓi/cZWLk - :r{19[)o&f!0Tf娙!wdi5ګ4^e?-x\"E{o_܂Ҟd-WiOO#Նeg9s)X%XhAf };{Š-E*t=~4W(ĸR[VJ)2 /0_*#V8]"5+Qf)NGEGN@tK's(ЪA.m'p1ݵtRKN905%\ZT-KW2}a%ؔeՐH/ J% B LO-q%݄>̛^JIoƗ f:t.=\|7j׶7w+ՙ.ӛ߹H`Dt;dtͮus˜?s{" o0#}w7ۀiY[r ж %% \n0z'Vw-أأb{:q,'6S}ɕAV­X U7ez<],R5tWiӶs_/wDuz>4p&1 ?627J峻$|_7)V&'MrٻMbb+Q+ҤV& m򀽏$6Kbm~m V&jY-.|-#%w4SXˏȻg=&jq.K>KuI|ZT^|NI=8ke1I6ϟ ><$>OnQD$|-^$!>$u~:g(l`=&qBwCHqy!5F nF5҅L/_"|Cs'Dx<oڏ_&\a5crξdL,Mgr,%hsߌ5%b|Y&o5,~ښjO7hB4b)R庄JxɛvhwR ǕR9մ\W5/t<,5TZ#&Zbv6Τ[}Ĥ+6bjIkG'6R]NFkp!{i{;_K}sG[oǙI endstream endobj 1211 0 obj << /Type /ObjStm /N 100 /First 882 /Length 2421 /Filter /FlateDecode >> stream x}Z]d }_qvֶdˆeaI %$y 顧vϏa鶮#U=iVRWkY~M fd`?¥T a R9fʸ3?~ Z% R[pn 5;c[J윁;ΑKe΄0s- yyرǁ!"㏕C$H(!+=_`;;g."vLag;W&WҘ xc I @ t\ Te4(32Y1sfVcd"#;S\6PTʌ.ИЄ5\B v #f7 U. ۹* Vk*ٯqb(`ts=֖޸ QįlZ2V~e[g~a\s䉭3Oy)AnTR2a> 10~Zx),JW\t"s ,\\Cޭ= #s0Ԩ &.Liӊ +xoA\*n,Hfb#?xy>eO_}rO~~ߝwS~Ă+LbɌX}ORk{4XɋG4T)V6[Mׇ<4郋+>8+J]A236NF3m6 !OZ2exk;ξ{9GWԕ=ɖҫFW1i~CNVlw˴כ9GC{5e]yC!c f|\LWו0r`ۆ툡gɵ-"o;ʵɞf2}Ri&F=Az|cFR|RbFw'6{;4h6AԝȺې*'aS"ەpr*{YFWr{R[h'CiOBkcS!yªQXO`b#i FW=Y z:&b„)~3?Mh=|_μ;lM&_d3 ~6dr{t6^Z AC۷O͊#Oߖu|?ȟ wy__^_^OkIxXD@o2?\??O0v_PW{xt(- _w E$ CG:rN>*QEbHPĠaPc41$>>o}0 mKņ' ٍR9OFc; K8Y$,+c811*Di\/rb6r'bض _>a\e Oz2j,2NFԸS},IxԴEy2Rg?`\?9St/"mjQ[d vd=.qOK0X$aHcOPzQb$^Uq%nKBLO1&dbVIť$uL1=5H=U3Bi=̈́>aqr0:0= 0*9׿&Y/{`EPg@9/ƅ"mmODM3Jvb XNՕS5(4!ERpaɇ!t,'icfr*.&s0ffE&>fa`r8 6H5qjc},.iazZ2>E,>a43ZE4Lj9̓fI ̋L^>.$S!M =fhI詺o$ URDWRD_pt uN[ cRMtGtQң }I/dFj&\j+uAkGԨ*+U(3Mn1Hh_D uAzh=]ISDS QՄ@Yng i7!{Cx}]ط/כ;*ڱ_UԕVK endstream endobj 1278 0 obj << /Author(\376\377\000N\000i\000c\000o\000l\000a\000s\000\040\000N\000i\000c\000l\000a\000u\000s\000s\000e)/Title(\376\377\000T\000s\000u\000n\000g\000\040\000D\000o\000c\000u\000m\000e\000n\000t\000a\000t\000i\000o\000n)/Subject()/Creator(LaTeX with hyperref package)/Producer(pdfTeX-1.40.17)/Keywords() /CreationDate (D:20170830090416+02'00') /ModDate (D:20170830090416+02'00') /Trapped /False /PTEX.Fullbanner (This is pdfTeX, Version 3.14159265-2.6-1.40.17 (TeX Live 2016) kpathsea version 6.2.2) >> endobj 1237 0 obj << /Type /ObjStm /N 41 /First 402 /Length 1619 /Filter /FlateDecode >> stream xڝYn7 W I 0 ͭiR4)I$bп/ɱ3kФHGQf!QBTB,&ԐDS@**@UH!l!dě D? JiRh8WdjRrH"QHe4Ot%d;碨h !CX*5e!k [DIr,d%`5Bx  @P55/@8d(R(L@r0Tbwxuxt{pw}P*;g{ͽR)Z}$QrdUZw/wvy̋2:yTOd}3u"!GVuR\H#"RH@ GWU*~ô\EjA ֚V>MzP=A{ub^ĩwJx7hLct9a-HhwUP;UPukW]q\3d $A+efj VwdXkg,q/E!TdO"U:_.^l\v֩^)u*WJ R׮ Yi?bR6Χ'yOc^m4}e[oCX ,A d f w Z nZN ଡ:ltCF*!ҙ[Ul5DS:I3*S@C蚒V]F2+I|֩HJפiDHI|. XG-4cvڌ\I5i<9&J*Z4`"u&hca#̾IDߦMQV{)2m#I_PݒͫhrMkҀĮT6)۪wa}Ӥ.sy9nsˉ*iur2&&t+cob5mFoA)&qDtMx 䚤bem۴`uJ ,XM>Ԣ Fq ԴIoo Cԓ|>f,MOi6Qu%6I[c<]yɷفviGڴO/H]ysD!jp(pɀfѷ䕘ZX\i_Z Q{ѤU,MloӎO>Ѥ$(H~&4=ϳDuZq6KRB_ "d-muWIm@8߮?S jP7~~swNsn1Mi!0b*4(0zgtԽ@C 94I浸%`EjSÜʗn^6 @5 7+')7йέrȭr|å'WS/6ou-7˫ϷW_nN"9_^RӼ,ߎ_1!T!k endstream endobj 1279 0 obj << /Type /XRef /Index [0 1280] /Size 1280 /W [1 3 1] /Root 1277 0 R /Info 1278 0 R /ID [<1A3E74CBF563DF9850BB3B213C6806DB> <1A3E74CBF563DF9850BB3B213C6806DB>] /Length 3135 /Filter /FlateDecode >> stream x%[l\W38Nlljc'vv_;v.NN;NNP !"T6lB+>f-!́<5i*P@6EX-3ӄ5` G;ECˡMւh5hu`A=Z m8&Є8fa3؊րv5n6!lhh wh;Nh w.=hݠm/nm^Ћ ~hC B;pC;PCh4Ezm-"E׏9vP QY!?i)oJquhYJ 4c?|hIdSh[ze)n4-zm?Z~(Rq\@[BI8.V68 |e]u^hd ־*X@F}hgж.%4Cֻn)`Mmo͕VT6{u ͞xV-l$=ٓ.&B6{R֋~K>FB6{m{5a'єzB6{m}h6є4y'ldb|74nٓQh*a'nٓ#hl'6{Bz\MB6{;ᫀ6{rMDfOx'j|ܺ=bIOk ;9NN.o2ص<`&W;4Y:`7&j1Z-*NS:Cs@'gyYXxIb 8L;w p"6R+mỳ 8J-S p'Xi*uN.UL݀p gOipLi?t) Δ$)Xoy$8ncPgI0pT{M9 n8,1M.yp,9sin[`\i X$n(S=|M.pI5J5 Zs I6TL tF3$$9S!܍c["nT`\fogSZo`F~}>ۦQH5Ui$>dҝ|d d$9#`3rLLgd:Ҙ]}G<r-kTEeKU{:[4+ڌtg*4zfO9E=v.+`O)l<RџՃp Ois"=媏[OhPKG멤}ncM/T͞rS sO=f410ƭ6NoF4MI+_N%](iTje Vǜ4ǝN MgVl L(.\ iՀí^MQrZ+|  OIq[O8d~i\ hb4.kqiTnhb+hL+6}Gu=ˊ[LB6qQ^k 57p\O!0 Zs#; w\9*_wp8\E85Nm.n֝$q)ḅp{GwIp\8 ,9.q/ptV;yǩᖁv77Ec*mj TP$`5_7ԯԘp)0CPʽ^o&3[nR8"p/xmt,ZGuXͽ艤3`~[؀> oŞVXSdiЊO<}V|o@O= Y6S/j Vȯ4boj QӬ'd+@ VsUĺjݒ.t{",@uE;A]H7E].@M,"cʅ[N:&L>)HFR| )>HAR| )>HAR| )>HAR| i )9OyJSrXCG+N?K@ endstream endobj startxref 626957 %%EOF tsung-1.7.0/docs/_templates/0000755000201100017670000000000013151315546015435 5ustar nniclausdreamtsung-1.7.0/docs/_templates/README0000644000201100017670000000001713151315546016313 0ustar nniclausdreamHTML Templates tsung-1.7.0/docs/_static/0000755000201100017670000000000013151315546014726 5ustar nniclausdreamtsung-1.7.0/docs/_static/README0000644000201100017670000000003713151315546015606 0ustar nniclausdreamStatic files used by make html tsung-1.7.0/docs/conf.py0000644000201100017670000001706313151315546014606 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-2017, 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.7.0' # The full version, including alpha/beta/rc tags. release = '1.7.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.7.0/docs/README0000644000201100017670000000273613151315546014170 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.7.0/docs/tsung-help.txt0000644000201100017670000000263213151315546016132 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.7.0/docs/Makefile0000644000201100017670000001267013151315546014746 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.7.0/docs/reports.rst0000644000201100017670000002055513151315546015537 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.7.0/docs/references.rst0000644000201100017670000000136513151315546016160 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.7.0/docs/proxy.rst0000644000201100017670000000350213151315546015213 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.7.0/docs/introduction.rst0000644000201100017670000000736613151315546016567 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.7.0/docs/installation.rst0000644000201100017670000000645313151315546016543 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.7.0/docs/index.rst0000644000201100017670000000176113151315546015146 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.7.0/docs/features.rst0000644000201100017670000001476413151315546015664 0ustar nniclausdream======== 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 :program:`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 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 versionning 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.7.0/docs/faq.rst0000644000201100017670000002745613151315546014617 0ustar nniclausdream.. index:: faq .. _faq: ========================== 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.7.0/docs/errorslist.rst0000644000201100017670000000405613151315546016247 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.7.0/docs/dtd.rst0000644000201100017670000000012213151315546014600 0ustar nniclausdream.. index:: dtd tsung-1.0.dtd ============= .. literalinclude:: ../tsung-1.0.dtd tsung-1.7.0/docs/conf-sessions.rst0000644000201100017670000011350513151315546016630 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 above, 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.7.0/src/templates/report.thtml0000644000201100017670000001372013151315546017525 0ustar nniclausdream[% INCLUDE header.thtml %]
[% USE format %] [% USE pf = format('%.5f') %]

Main Statistics

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "stats" %] [% END %] [% END %]
Name highest 10sec mean lowest 10sec mean Highest RateMean Rate Mean Count
$key [% data.maxmean.$key %] [% data.minmean.$key %] [% data.rate.$key %] / sec [% data.rate_mean.$key %] / sec [% data.mean.$key %] [% data.count.$key %]

Transactions Statistics

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "transaction" %] [% END %] [% END %]
Name highest 10sec mean lowest 10sec meanHighest RateMean Rate Mean Count
$key [% data.maxmean.$key %] [% data.minmean.$key %] [% data.rate.$key %] / sec [% data.rate_mean.$key %] / sec [% data.mean.$key %] [% data.count.$key %]

Network Throughput

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "network" %] [% END %] [% END %]
Name Highest RateTotal
$key [% data.rate.$key %]/sec [% data.maxmean.$key %]

Counters Statistics

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "count" or cat_data.$key == "match" %] [% END %] [% END %]
Name Highest RateMean RateTotal number
$key [% data.rate.$key %] / sec [% data.rate_mean.$key %] / sec [% data.maxmean.$key %]

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "gauge" %] [% END %] [% END %]
Name Max
$key [% data.maxmean.$key %]

[% IF errors %]

Errors

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "error" %] [% END %] [% END %]
Name Highest RateTotal number
$key [% data.rate.$key %] / sec [% data.maxmean.$key %]
[% END %] [% IF os_mon %]

Server monitoring

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "os_mon_cpu" %] [% END %] [% IF cat_data.$key == "os_mon_load" %] [% END %] [% IF cat_data.$key == "os_mon_free" %] [% END %] [% IF cat_data.$key == "os_mon_packets" %] [% END %] [% IF cat_data.$key == "os_mon_other" %] [% END %] [% END %]
Name highest 10sec meanlowest 10sec mean
$key [% data.maxmean.$key %] % [% data.minmean.$key %] %
$key [% data.maxmean.$key %] [% data.minmean.$key %]
$key [% data.maxmean.$key %] MB [% data.minmean.$key %] MB
$key [% data.maxmean.$key %] / sec [% data.minmean.$key %] / sec
$key [% data.maxmean.$key %] / sec [% data.minmean.$key %] / sec
[% END %] [% IF http %]

HTTP return code

[% FOREACH key = data.rate.keys.sort %] [% IF cat_data.$key == "http_status" %] [% END %] [% END %]
Code Highest RateMean RateTotal number
$key [% data.rate.$key %] / sec [% data.rate_mean.$key %] / sec [% data.maxmean.$key %]
[% END %] [% INCLUDE footer.thtml %] tsung-1.7.0/src/templates/footer.thtml0000644000201100017670000000046413151315546017511 0ustar nniclausdream
tsung-1.7.0/src/tsung_recorder/0000755000201100017670000000000013151461561016163 5ustar nniclausdreamtsung-1.7.0/src/tsung_recorder/ts_recorder_sup.erl0000644000201100017670000000631713151315546022101 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.7.0/src/tsung_recorder/tsung_recorder.erl0000644000201100017670000000570113151315546021720 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.7.0/src/tsung_recorder/ts_proxy_webdav.erl0000644000201100017670000001401613151315546022111 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.7.0/src/tsung_recorder/ts_proxy_http.erl0000644000201100017670000004371213151315546021625 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.7.0/src/tsung_recorder/ts_client_proxy_sup.erl0000644000201100017670000000604313151315546023007 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_recorder/ts_proxy_listener.erl0000644000201100017670000002067213151315546022473 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 everytime 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.7.0/src/tsung_recorder/ts_proxy_recorder.erl0000644000201100017670000002074313151315546022452 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.7.0/src/tsung_recorder/ts_client_proxy.erl0000644000201100017670000002262113151315546022120 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("Uknown 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.7.0/src/tsung_recorder/ts_proxy_pgsql.erl0000644000201100017670000004721113151315546021772 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.7.0/src/tsung_recorder/tsung_recorder.app.in0000644000201100017670000000145713151315546022327 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.7.0/src/test/0000755000201100017670000000000013151461561014115 5ustar nniclausdreamtsung-1.7.0/src/test/ipcfg.out0000644000201100017670000000106613151315546015742 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.7.0/src/test/ifcfg.out0000644000201100017670000000425713151315546015735 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.7.0/src/test/procnetdev_test.txt0000644000201100017670000000127013151315546020067 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.7.0/src/test/procnetdev_test7chars.txt0000644000201100017670000000127113151315546021200 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.7.0/src/test/netstat_test.txt0000644000201100017670000000055213151315546017402 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.7.0/src/test/netstat_test3.txt0000644000201100017670000000254713151315546017473 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.7.0/src/test/netstat_test2.txt0000644000201100017670000000725313151315546017471 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.7.0/src/test/test_file_server_pipe.csv0000644000201100017670000000010413151315546021207 0ustar nniclausdreamconv%2F99%2F589%2Finfo.txt|99|589 conv%2F99%2F938%2Finfo.txt|99|938 tsung-1.7.0/src/test/test_file_server.csv0000644000201100017670000000005513151315546020177 0ustar nniclausdreamusername1;glop; username2;; username3;glop4; tsung-1.7.0/src/test/test_file_server2.csv0000644000201100017670000000001513151315546020255 0ustar nniclausdreamuser1;sesame tsung-1.7.0/src/test/xmpp-muc.xml.in0000644000201100017670000001101413151315546017010 0ustar nniclausdream tsung-1.7.0/src/test/badpop.xml.in0000644000201100017670000000306313151315546016514 0ustar nniclausdream tsung-1.7.0/src/test/thinkfirst.xml.in0000644000201100017670000000371313151315546017436 0ustar nniclausdream tsung-1.7.0/src/test/ts_test_options.erl0000644000201100017670000000127513151315546020067 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.7.0/src/test/ts_test_http.erl0000644000201100017670000004037213151315546017354 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). 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_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.7.0/src/test/ts_test_mochi.erl0000644000201100017670000000070513151315546017470 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.7.0/src/test/ts_test_pgsql.erl0000644000201100017670000001477313151315546017531 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.7.0/src/test/ts_test_mon.erl0000644000201100017670000000711513151315546017164 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.7.0/src/test/ts_test_client.erl0000644000201100017670000001121113151315546017641 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.7.0/src/test/ts_test_config.erl0000644000201100017670000002737613151315546017653 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 probabilites 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 probabilites and weights",0,0}, ts_config:get_popularity(0,0,true,100)), ?assertError({"can't mix probabilites 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.7.0/src/test/ts_test_match.erl0000644000201100017670000001151013151315546017461 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.7.0/src/test/ts_test_dynvars_api.erl0000644000201100017670000000555613151315546020721 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.7.0/src/test/ts_test_interaction.erl0000644000201100017670000000271413151315546020712 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.7.0/src/test/ts_test_utils.erl0000644000201100017670000001075113151315546017533 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(["2013","10","17",45,"19","41"],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.7.0/src/test/ts_test_proxy.erl0000644000201100017670000001715613151315546017562 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.7.0/src/test/ts_test_file_server.erl0000644000201100017670000000672413151315546020705 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.7.0/src/test/ts_test_mqtt.erl0000644000201100017670000001374713151315546017370 0ustar nniclausdream%%% This code was developped 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("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). 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}, EncodedData = mqtt_frame:encode(Message), ?assertEqual(<<128,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.7.0/src/test/ts_test_websocket.erl0000644000201100017670000001173613151315546020365 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.7.0/src/test/ts_test_jabber.erl0000644000201100017670000003275013151315546017623 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.7.0/src/test/ts_test_search.erl0000644000201100017670000005067413151315546017650 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} ],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} ],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} ],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} ],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} ],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} ],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} ],list_to_binary(Data))), JSONPathTab = "menu.popup.menuitem[0].value", ?assertEqual([{'myvar', << "New" >>}], ts_search:parse_dynvar([{jsonpath,'myvar', JSONPathTab} ],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} ],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} ],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} ],list_to_binary(Data))). 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.7.0/src/test/ts_test_user_server.erl0000644000201100017670000000634613151315546020744 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.7.0/src/test/ts_test_all.erl0000644000201100017670000001445513151315546017150 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" "
" ]). 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.7.0/src/test/ts_test_stats.erl0000644000201100017670000000123413151315546017525 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.7.0/src/test/ts_test_recorder.erl0000644000201100017670000001006513151315546020176 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.7.0/src/tsung_controller/0000755000201100017670000000000013151461561016541 5ustar nniclausdreamtsung-1.7.0/src/tsung_controller/tsung_controller.app.in0000644000201100017670000000757213151315546023267 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.7.0/src/tsung_controller/ts_config_mqtt.erl0000644000201100017670000001213013151315546022263 0ustar nniclausdream%%% This code was developped 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), 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), 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}, 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.7.0/src/tsung_controller/ts_timer.erl0000644000201100017670000001717213151315546021104 0ustar nniclausdream%%% This code was developped 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 immediatly 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.7.0/src/tsung_controller/ts_config_mysql.erl0000644000201100017670000000743513151315546022457 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 datas: 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.7.0/src/tsung_controller/ts_msg_server.erl0000644000201100017670000001145313151315546022134 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_controller/ts_config_jabber.erl0000644000201100017670000002737613151315546022545 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.7.0/src/tsung_controller/ts_config_job.erl0000644000201100017670000000776213151315546022067 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.7.0/src/tsung_controller/ts_config_server.erl0000644000201100017670000013237613151315546022623 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), {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), {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 dictionnary % 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 environnement 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, otherwize 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.7.0/src/tsung_controller/ts_file_server.erl0000644000201100017670000002061313151315546022263 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.7.0/src/tsung_controller/ts_user_server.erl0000644000201100017670000005034213151315546022324 0ustar nniclausdream%%% This code was developped 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 whos 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 beginnig 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.7.0/src/tsung_controller/ts_config.erl0000644000201100017670000017564413151315546021242 0ustar nniclausdream%%% This code was developped 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} -> {gt,Gte}; {none, none, none, none, none, Lte} -> {lt,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), InitialAction = {ctrl_struct, {if_start, Rel, VarName, list_to_binary(Value) , 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} -> {gt,Gte}; {none, none, none, none, none, Lte} -> {lt,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}) -> 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}; _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 ~n",[CType, Server,Port,PType,Id],?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; "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); "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); "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; "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; "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 probabilites 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.7.0/src/tsung_controller/ts_stats_mon.erl0000644000201100017670000004143113151315546021766 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 dictionnary %% 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.7.0/src/tsung_controller/ts_config_http.erl0000644000201100017670000005117613151315546022272 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]). -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 informations 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), ?LOGF("DIGEST ? : ~p ~p ~p", [Type, Nonce, Realm], ?WARN), 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. 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 Substition 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 informations 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 absolut 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) -> % 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.7.0/src/tsung_controller/ts_os_mon_erlang.erl0000644000201100017670000003360513151315546022605 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.7.0/src/tsung_controller/ts_os_mon_sup.erl0000644000201100017670000000531113151315546022135 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.7.0/src/tsung_controller/ts_interaction_server.erl0000644000201100017670000002055413151315546023667 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.7.0/src/tsung_controller/ts_config_raw.erl0000644000201100017670000000647613151315546022107 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.7.0/src/tsung_controller/ts_os_mon_snmp.erl0000644000201100017670000002535613151315546022316 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.7.0/src/tsung_controller/ts_config_ldap.erl0000644000201100017670000002061513151315546022225 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.7.0/src/tsung_controller/ts_config_fs.erl0000644000201100017670000000632513151315546021717 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.7.0/src/tsung_controller/ts_job_notify.erl0000644000201100017670000003104513151315546022121 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.7.0/src/tsung_controller/ts_config_websocket.erl0000644000201100017670000000713413151315546023274 0ustar nniclausdream%%% This code was developped 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"), Request = #websocket_request{data = ValRaw, type = Type, subprotos = SubProtocols, origin = Origin, path = Path, frame = Frame}, 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.7.0/src/tsung_controller/ts_config_shell.erl0000644000201100017670000000554413151315546022420 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.7.0/src/tsung_controller/ts_web.erl0000644000201100017670000003612313151315546020536 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, 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.7.0/src/tsung_controller/ts_config_pgsql.erl0000644000201100017670000001700013151315546022425 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.7.0/src/tsung_controller/ts_os_mon.erl0000644000201100017670000000630213151315546021247 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_controller/ts_match_logger.erl0000644000201100017670000001665213151315546022421 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.7.0/src/tsung_controller/ts_mon.erl0000644000201100017670000005406513151315546020557 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_controller/ts_config_amqp.erl0000644000201100017670000002152113151315546022240 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_controller/ts_controller_sup.erl0000644000201100017670000001603213151315546023030 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_controller/ts_user_server_sup.erl0000644000201100017670000000365613151315546023221 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.7.0/src/tsung_controller/ts_os_mon_munin.erl0000644000201100017670000002707713151315546022471 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 succesful 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} ) -> %% Currenly, 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.7.0/src/tsung_controller/tsung_controller.erl0000644000201100017670000001513413151315546022655 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung_controller/ts_api.erl0000644000201100017670000000412313151315546020525 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.7.0/src/lib/0000755000201100017670000000000013151461561013704 5ustar nniclausdreamtsung-1.7.0/src/lib/rabbit_binary_generator.erl0000644000201100017670000002532713151315546021277 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.7.0/src/lib/oauth_rsa_sha1.erl0000644000201100017670000000444613151315546017322 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.7.0/src/lib/pgsql_util.erl0000644000201100017670000001754613151315546016611 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.7.0/src/lib/mochiweb_xpath.erl0000644000201100017670000004511013151315546017413 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.7.0/src/lib/mochiweb_xpath_functions.erl0000644000201100017670000001211113151315546021476 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 responsable 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.7.0/src/lib/rabbit_misc.erl0000644000201100017670000000167413151315546016677 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.7.0/src/lib/rabbit_framing_amqp_0_9_1.erl0000644000201100017670000017434713151315546021304 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.7.0/src/lib/mochiweb_xpath_utils.erl0000644000201100017670000000472513151315546020642 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.7.0/src/lib/oauth_plaintext.erl0000644000201100017670000000264113151315546017624 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.7.0/src/lib/mochiutf8.erl0000644000201100017670000003051113151315546016317 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.7.0/src/lib/mochiweb_headers.erl0000644000201100017670000003725013151315546017710 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.7.0/src/lib/mochiweb_html.erl0000644000201100017670000007117313151315546017243 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.7.0/src/lib/oauth.erl0000644000201100017670000001344713151315546015542 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.7.0/src/lib/mochijson2.erl0000644000201100017670000007674013151315546016502 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.7.0/src/lib/mochiweb_charref.erl0000644000201100017670000020623413151315546017707 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.7.0/src/lib/oauth_uri.erl0000644000201100017670000001050513151315546016411 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.7.0/src/lib/mqtt_frame.erl0000644000201100017670000003372213151315546016557 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.7.0/src/lib/mochinum.erl0000644000201100017670000002647613151315546016247 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.7.0/src/lib/mochiweb_xpath_parser.erl0000644000201100017670000000445313151315546020774 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.7.0/src/lib/mochiweb_util.erl0000644000201100017670000010145613151315546017252 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 explicitily 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.7.0/src/lib/rabbit_binary_parser.erl0000644000201100017670000000736113151315546020603 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.7.0/src/lib/websocket.erl0000644000201100017670000001740313151315546016404 0ustar nniclausdream%%% This code was developped 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/5, 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) -> {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"]), 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 shoud 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. tsung-1.7.0/src/lib/pgsql_proto.erl0000644000201100017670000005232013151315546016764 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 doesnt %% 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 apropriate %% 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.7.0/src/lib/oauth_unix.erl0000644000201100017670000000266713151315546016607 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.7.0/src/lib/uuid.erl0000644000201100017670000001327713151315546015371 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.7.0/src/lib/rfc4515_parser.erl0000644000201100017670000001207113151315546017057 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.7.0/src/lib/oauth_http.erl0000644000201100017670000000505113151315546016571 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.7.0/src/lib/rabbit_command_assembler.erl0000644000201100017670000001350313151315546021411 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.7.0/src/lib/oauth_hmac_sha1.erl0000644000201100017670000000340213151315546017434 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.7.0/src/tsung/0000755000201100017670000000000013151461561014276 5ustar nniclausdreamtsung-1.7.0/src/tsung/tsung.app.in0000644000201100017670000001125413151315546016551 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.7.0/src/tsung/ts_digest.erl0000644000201100017670000000667613151315546017007 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.7.0/src/tsung/ts_udp.erl0000644000201100017670000000436113151315546016305 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.7.0/src/tsung/ts_mqtt.erl0000644000201100017670000003354413151315546016507 0ustar nniclausdream%%% This code was developped 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(#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}, #state_rcv{session = MqttSession}) -> ClientId = ["tsung-", ts_utils:randombinstr(10)], 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}, #state_rcv{session = MqttSession = #mqtt_session{curr_id = Id}}) -> NewMqttSession = case Qos of 0 -> MqttSession; _ -> MqttSession#mqtt_session{curr_id = Id + 1} end, MsgId = NewMqttSession#mqtt_session.curr_id, Message = #mqtt{id = MsgId, type = ?PUBLISH, qos = Qos, retain = Retained, arg = {Topic, Payload}}, 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}, {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}, 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}), 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}, _HostData) -> NewUserName = ts_search:subst(UserName, DynVars), NewPassword = ts_search:subst(Password, DynVars), Param#mqtt_request{ type = connect, clean_start = CleanStart, keepalive = KeepAlive, will_topic = WillTopic, will_qos = WillQos, will_msg = WillMsg, will_retain = WillRetain, username = NewUserName, password = NewPassword }; 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. tsung-1.7.0/src/tsung/ts_websocket.erl0000644000201100017670000001714313151315546017505 0ustar nniclausdream%%% This code was developped 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, origin = Origin}, State=#state_rcv{session = WebsocketSession}) -> {Request, Accept} = websocket:get_handshake(State#state_rcv.host, Path, SubProtocol, Version, Origin), {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}, _HostData) -> NewPath = ts_search:subst(Path, DynVars), Param#websocket_request{path = NewPath}; add_dynparams(_Bool, _DynData, Param, _HostData) -> Param#websocket_request{}. tsung-1.7.0/src/tsung/ts_mysql.erl0000644000201100017670000002605213151315546016663 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 %%% Dependancies: none %%% Note: Packet fragmentation isnt 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.7.0/src/tsung/gen_ts_transport.erl0000644000201100017670000000266013151315546020402 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. tsung-1.7.0/src/tsung/ts_jabber.erl0000644000201100017670000004007613151315546016745 0ustar nniclausdream%%% This code was developped 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 %% Accomodates 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.7.0/src/tsung/ts_search.erl0000644000201100017670000005272113151315546016765 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 / Differenciated 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]). -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([], _Data) -> ts_dynvars:new(); parse_dynvar(DynVarSpecs, Data) when is_binary(Data) -> ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]), parse_dynvar(DynVarSpecs,Data, undefined,undefined,[]); parse_dynvar(DynVarSpecs, {_,_,_,Data}) when is_binary(Data) -> ?DebugF("Parsing Dyn Variable (specs=~p); data is ~p~n",[DynVarSpecs,Data]), parse_dynvar(DynVarSpecs,Data, undefined,undefined,[]); parse_dynvar(DynVarSpecs, {_,_,_,Data}) 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,[]); parse_dynvar(DynVarSpecs, _Data) -> ?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],?ERR), parse_dynvar(D,Binary,String,xpath_error,DynVars) end; parse_dynvar(D=[{jsonpath,_VarName, _Expr}| _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), parse_dynvar(DynVarsSpecs, Binary,String,xpath_error,DynVars); parse_dynvar([{jsonpath,VarName,_Expr}|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}| DynVarsSpecs],Binary,String,JSON,DynVars)-> Values = case ts_utils:jsonpath(Expr,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.7.0/src/tsung/ts_cport.erl0000644000201100017670000001430513151315546016643 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 dictionnary 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.7.0/src/tsung/ts_client.erl0000644000201100017670000017621613151315546017004 0ustar nniclausdream%%% This code was developped 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 connexion 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(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, 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_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 occured) ?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 reponse 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), case Request#ts_request.endpage of true -> % end of a page, compute page reponse 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.7.0/src/tsung/ts_job.erl0000644000201100017670000002064613151315546016273 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.7.0/src/tsung/ts_local_mon.erl0000644000201100017670000001500213151315546017452 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.7.0/src/tsung/ts_ip_scan.erl0000644000201100017670000001674213151315546017137 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.7.0/src/tsung/ts_stats.erl0000644000201100017670000001301013151315546016642 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung/ts_client_sup.erl0000644000201100017670000000616013151315546017661 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung/ts_mon_cache.erl0000644000201100017670000002221713151315546017431 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.7.0/src/tsung/ts_erlang.erl0000644000201100017670000000447713151315546016775 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.7.0/src/tsung/ts_sup.erl0000644000201100017670000000756013151315546016330 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung/ts_ssl6.erl0000644000201100017670000000440213151315546016400 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.7.0/src/tsung/ts_http.erl0000644000201100017670000003363013151315546016475 0ustar nniclausdream%%% This code was developped 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). %%---------------------------------------------------------------------- %% 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},ungreedy,dotall]) of nomatch -> Data; {match, [Header,Body]} -> {Header,<< Body/binary,"\n" >>}; _ -> Data end. tsung-1.7.0/src/tsung/ts_session_cache.erl0000644000201100017670000002033513151315546020322 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.7.0/src/tsung/ts_plugin.erl0000644000201100017670000000442613151315546017015 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.7.0/src/tsung/tsung.erl0000644000201100017670000000457113151315546016152 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung/ts_ldap_common.erl0000644000201100017670000001615713151315546020013 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 appropiate %% 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.7.0/src/tsung/ts_dynvars.erl0000644000201100017670000001131313151315546017176 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). tsung-1.7.0/src/tsung/ts_launcher_static.erl0000644000201100017670000002166013151315546020666 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.7.0/src/tsung/ts_tcp.erl0000644000201100017670000000561213151315546016303 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_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.7.0/src/tsung/ts_ldap.erl0000644000201100017670000003012113151315546016426 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 %% appropiate 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 possitive response to a startTLS command, inmediatly 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 perfom substitutions on the filter of the search requests. %% The filter text was already parsed into a tree-like struct, substitution %% is perfomed 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.7.0/src/tsung/ts_server_websocket.erl0000644000201100017670000001534213151315546021072 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 ? {Handshake, Accept} = websocket:get_handshake(Host, Path, SubProtocol, Version, Origin), 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(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.7.0/src/tsung/ts_ssl_session_cache.erl0000644000201100017670000000542313151315546021204 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.7.0/src/tsung/ts_fs.erl0000644000201100017670000002716013151315546016127 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 diretory: ~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 diretory: ~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.7.0/src/tsung/ts_tcp6.erl0000644000201100017670000000373413151315546016374 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.7.0/src/tsung/ts_bosh.erl0000644000201100017670000006026613151315546016456 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.7.0/src/tsung/ts_http_common.erl0000644000201100017670000007642113151315546020052 0ustar nniclausdream%%% This code was developped 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}; 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 tranfer 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.7.0/src/tsung/ts_shell.erl0000644000201100017670000001337513151315546016631 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.7.0/src/tsung/ts_server_websocket_ssl.erl0000644000201100017670000001524313151315546021753 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, 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 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, 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, state = not_connected} = State)-> Origin ="", {Handshake, Accept} = websocket:get_handshake(Host, Path, SubProtocol, Version, Origin), 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 {tcp, 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; {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, 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); {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(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.7.0/src/tsung/ts_raw.erl0000644000201100017670000001043513151315546016305 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung/ts_bosh_ssl.erl0000644000201100017670000000403513151315546017327 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.7.0/src/tsung/ts_amqp.erl0000644000201100017670000006352013151315546016455 0ustar nniclausdream%%% This code was developped 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.7.0/src/tsung/ts_pgsql.erl0000644000201100017670000003470013151315546016643 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 %%% Dependancies: 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.7.0/src/tsung/ts_launcher.erl0000644000201100017670000005205613151315546017322 0ustar nniclausdream%%% This code was developped 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 independant 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("Substract 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.7.0/src/tsung/ts_utils.erl0000644000201100017670000011566513151315546016667 0ustar nniclausdream%%% This code was developped 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 environnement 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 environnement 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 occurence 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 occurence 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, {timout, 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, ?LOG("ok~n",?ERR), 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.7.0/src/tsung/ts_ssl.erl0000644000201100017670000000425013151315546016313 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{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_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.7.0/src/tsung/ts_udp6.erl0000644000201100017670000000372613151315546016377 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.7.0/src/tsung/ts_jabber_common.erl0000644000201100017670000010264213151315546020313 0ustar nniclausdream%%% This code was developped 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}) -> ts_user_server:remove_connected(UserServer,set_id(Id,User,Pwd)), close(); 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); %% 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=#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 = Version}) -> VersionStr = case Version of "legacy" -> ""; V when is_list(V) -> "version='" ++ Version ++"' " end, list_to_binary([ ""]). %%---------------------------------------------------------------------- %% Func: close/0 %% Purpose: close jabber session %%---------------------------------------------------------------------- close () -> list_to_binary(""). %%---------------------------------------------------------------------- %% 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, "' "]. %%%---------------------------------------------------------------------- %%% 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). %% 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.7.0/src/tsung/ts_launcher_mgr.erl0000644000201100017670000002017513151315546020164 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.7.0/src/tsung/ts_webdav.erl0000644000201100017670000000661713151315546016773 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). %%% methode PROPFIND; entetes: Depth (optionel); body: XML %%% methode COPY; entete Destination: URL, If (optionel), Overwrite (Optionel), Depth; Body: XML (Optionel) %%% methode MOVE; entete Destination: URL, If (optionel), Overwrite (Optionel), Depth; Body: XML (Optionel) %%% methode PROPPATCH body: XML %%% methode MKCOL %%% methode LOCK; entete: Timeout (optionel ?), If (Optionel),Depth (Optionel); Body: XML (optionel ?) %%% methode UNLOCK; entete: Lock-Token; Body: XML (optionel ?)