summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--network/ClusterSSH/ClusterSSH.SlackBuild105
-rw-r--r--network/ClusterSSH/ClusterSSH.info11
-rw-r--r--network/ClusterSSH/README9
-rw-r--r--network/ClusterSSH/patches/00-4.01_05.diff1641
-rw-r--r--network/ClusterSSH/slack-desc19
5 files changed, 1785 insertions, 0 deletions
diff --git a/network/ClusterSSH/ClusterSSH.SlackBuild b/network/ClusterSSH/ClusterSSH.SlackBuild
new file mode 100644
index 0000000000..245cd886b3
--- /dev/null
+++ b/network/ClusterSSH/ClusterSSH.SlackBuild
@@ -0,0 +1,105 @@
+#!/bin/sh
+#
+# Copyright (c) 2011, 2012, 2013 LEVAI Daniel
+# 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE AUTHOR BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 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.
+
+
+PRGNAM=ClusterSSH
+VERSION=${VERSION:-4.01_05}
+BUILD=${BUILD:-1}
+TAG=${TAG:-_SBo}
+
+SRCNAM="App-${PRGNAM}"
+SRCVER="4.01_01"
+
+if [ -z "$ARCH" ]; then
+ case "$( uname -m )" in
+ i?86) ARCH=i486 ;;
+ arm*) ARCH=arm ;;
+ *) ARCH=$( uname -m ) ;;
+ esac
+fi
+
+CWD=$(pwd)
+TMP=${TMP:-/tmp/SBo}
+PKG=$TMP/package-$PRGNAM
+OUTPUT=${OUTPUT:-/tmp}
+
+if [ "$ARCH" = "i486" ]; then
+ SLKCFLAGS="-O2 -march=i486 -mtune=i686"
+ LIBDIRSUFFIX=""
+elif [ "$ARCH" = "i686" ]; then
+ SLKCFLAGS="-O2 -march=i686 -mtune=i686"
+ LIBDIRSUFFIX=""
+elif [ "$ARCH" = "x86_64" ]; then
+ SLKCFLAGS="-O2 -fPIC"
+ LIBDIRSUFFIX="64"
+else
+ SLKCFLAGS="-O2"
+ LIBDIRSUFFIX=""
+fi
+
+set -e
+
+rm -rf $PKG
+mkdir -p $TMP $PKG $OUTPUT
+cd $TMP
+rm -rf $SRCNAM-$SRCVER
+tar xvf $CWD/$SRCNAM-$SRCVER.tar.gz
+cd $SRCNAM-$SRCVER
+chown -R root:root .
+find . \
+ \( -perm 777 -o -perm 775 -o -perm 711 -o -perm 555 -o -perm 511 \) \
+ -exec chmod 755 {} \; -o \
+ \( -perm 666 -o -perm 664 -o -perm 600 -o -perm 444 -o -perm 440 -o -perm 400 \) \
+ -exec chmod 644 {} \;
+
+for diff in $CWD/patches/*.diff; do
+ patch -E --quiet -p1 < "${diff}"
+done
+
+perl Build.PL \
+ prefix=/usr \
+ installdirs=vendor \
+ destdir=$PKG
+./Build
+./Build test
+./Build install \
+ --install_path bindoc=/usr/man/man1 \
+ --install_path libdoc=/usr/man/man3
+
+find $PKG/usr/man -type f -exec gzip -9 {} \;
+for i in $( find $PKG/usr/man -type l ) ; do ln -s $( readlink $i ).gz $i.gz ; rm $i ; done
+
+find $PKG -name perllocal.pod -o -name ".packlist" -o -name "*.bs" | xargs rm -f || true
+
+find $PKG -depth -type d -empty -delete || true
+
+mkdir -p $PKG/usr/doc/$PRGNAM-$VERSION
+cp -a Changes README $PKG/usr/doc/$PRGNAM-$VERSION
+cat $CWD/$PRGNAM.SlackBuild > $PKG/usr/doc/$PRGNAM-$VERSION/$PRGNAM.SlackBuild
+
+mkdir -p $PKG/install
+cat $CWD/slack-desc > $PKG/install/slack-desc
+
+cd $PKG
+/sbin/makepkg -l y -c n $OUTPUT/$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.${PKGTYPE:-tgz}
diff --git a/network/ClusterSSH/ClusterSSH.info b/network/ClusterSSH/ClusterSSH.info
new file mode 100644
index 0000000000..ce529b64f9
--- /dev/null
+++ b/network/ClusterSSH/ClusterSSH.info
@@ -0,0 +1,11 @@
+PRGNAM="ClusterSSH"
+VERSION="4.01_05"
+HOMEPAGE="http://clusterssh.sourceforge.net"
+DOWNLOAD="http://downloads.sourceforge.net/clusterssh/App-ClusterSSH-4.01_01.tar.gz"
+MD5SUM="2adcd457d0647409c8948df68d26b155"
+DOWNLOAD_x86_64=""
+MD5SUM_x86_64=""
+REQUIRES="perl-Try-Tiny perl-x11-protocol perl-File-Which perl-Test-Pod perl-Test-Pod-Coverage \
+ perl-tk perl-Test-Trap perl-Exception-Class perl-Readonly perl-Test-DistManifest perl-Test-Differences"
+MAINTAINER="LEVAI Daniel"
+EMAIL="leva@ecentrum.hu"
diff --git a/network/ClusterSSH/README b/network/ClusterSSH/README
new file mode 100644
index 0000000000..992ddcc0bf
--- /dev/null
+++ b/network/ClusterSSH/README
@@ -0,0 +1,9 @@
+ClusterSSH is a tool for making the same change on multiple servers at the same
+time. The 'cssh' command opens an administration console and an xterm to all
+specified hosts. Any text typed into the administration console is replicated
+to all windows. All windows may also be typed into directly.
+
+This tool is intended for (but not limited to) cluster administration where the
+same configuration or commands must be run on each node within the cluster.
+Performing these commands all at once via this tool ensures all nodes are kept
+in sync.
diff --git a/network/ClusterSSH/patches/00-4.01_05.diff b/network/ClusterSSH/patches/00-4.01_05.diff
new file mode 100644
index 0000000000..d7b05cf631
--- /dev/null
+++ b/network/ClusterSSH/patches/00-4.01_05.diff
@@ -0,0 +1,1641 @@
+diff --git a/Build.PL b/Build.PL
+index be3afb3..467c201 100644
+--- a/Build.PL
++++ b/Build.PL
+@@ -6,7 +6,7 @@ use Module::Build;
+ my $build = Module::Build->new(
+ meta_merge => {
+ resources => {
+- repository => [
++ Repository => [
+ 'http://clusterssh.git.sourceforge.net/',
+ 'http://github.com/duncs/clusterssh',
+ ],
+@@ -34,6 +34,10 @@ my $build = Module::Build->new(
+ 'File::Which' => 0,
+ 'File::Temp' => 0,
+ 'Test::DistManifest' => 0,
++ 'Test::Differences' => 0,
++ },
++ configure_requires => {
++ 'Module::Build' => 0,
+ },
+ add_to_cleanup => ['App-ClusterSSH-*'],
+ create_makefile_pl => 'traditional',
+diff --git a/Changes b/Changes
+index c1fe7e9..485932d 100644
+--- a/Changes
++++ b/Changes
+@@ -1,3 +1,34 @@
++2013-03-05 Duncan Ferguson <duncan_ferguson@user.sf.net> - v4.01_05
++- New option (-m, --unique-servers) to remove repeated servers when openeing terminals (Thanks to Oliver Meissner)
++- Drop MYMETA.yml and .json files from the distribution
++- Do not set default user name to prevent overriding ssh configuration
++
++2013-02-26 Duncan Ferguson <duncan_ferguson@user.sf.net> - v4.01_04
++- Fixed 'ccon' not calling the correct command (Sf bug 3605002)
++- Fixed clusters not being defined correctly within the .clusterssh/config file (Sf bug 3605675)
++
++2013-02-15 Duncan Ferguson <duncan_ferguson@user.sf.net> - v4.01_03
++* Correct documentation for references to $HOME/.clusterssh/config
++* Re-add user back into the configurartion file
++* Add in missing newline for some error messages
++* Allow the path to rsh/ssh/telnet to be defined in the configuration file
++* Move .csshrc to .csshrc.DISABLED since it should no longer be used
++* Error emitted when adding a host via the "Hosts" drop-down (Debian bug ID #578208)
++* Pastes uses a strange keyboard layout (Debian bug ID #364565)
++* Cope with being invoked by 'clusterssh' (Debian bug ID #644368)
++* Fix migration of .csshrc when not working as expected (Debian bug ID #673507)
++* Remove doc references to 'always_tile' as renamed 'window_tiling' (Debian bug ID #697371)
++* Updated manpage whatis entries (patch by Tony Mancill)
++* Fix watch line expression to catch 4.x series tarballs (Debian patch LP ID #1076897)
++* Allow tests to pass successfully when run as root
++* Fix cssh starting if xterm is not installed (Sf bug 3494988)
++* Set WM_CLASS on windows to 'cssh' (Sf bug 3187736)
++
++2012-12-09 Duncan Ferguson <duncan_ferguson@user.sf.net> - v4.01_02
++* Fix logic when using 'autoclose' on the command line or config file
++* Fix $HOME/.clusterssh/clusters being read in
++* Fix 'ctel', 'crsh' and 'ccon'so they work as expected
++
+ 2011-12-09 Duncan Ferguson <duncan_ferguson@user.sf.net> - v4.01_01
+ * Include missing files from release tarballs
+
+diff --git a/MANIFEST b/MANIFEST
+index cebe4b1..a4b4983 100644
+--- a/MANIFEST
++++ b/MANIFEST
+@@ -14,13 +14,11 @@ lib/App/ClusterSSH/Helper.pm
+ lib/App/ClusterSSH/Host.pm
+ lib/App/ClusterSSH/L10N.pm
+ lib/App/ClusterSSH/L10N/en.pm
+-Makefile
+-Makefile.old
+ Makefile.PL
+ MANIFEST
+ MANIFEST.SKIP
++META.json
+ META.yml
+-MYMETA.json
+ README
+ t/00-load.t
+ t/01l10n.t
+diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP
+index 3753e38..4900842 100644
+--- a/MANIFEST.SKIP
++++ b/MANIFEST.SKIP
+@@ -4,7 +4,11 @@
+ ^Build$
+ ^.git/
+ ^.gitignore
++^Makefile$
++^Makefile.old$
+ ^MANIFEST\.bak$
+-^MYMETA.yml$
++MYMETA.json
++MYMETA.yml
++pm_to_blib
+ .*\.swp$
+ ^WIP_TASKS$
+diff --git a/META.json b/META.json
+new file mode 100644
+index 0000000..26b6d74
+--- /dev/null
++++ b/META.json
+@@ -0,0 +1,94 @@
++{
++ "abstract" : "A container for functions of the ClusterSSH programs",
++ "author" : [
++ "Duncan Ferguson <duncan_j_ferguson@yahoo.co.uk>"
++ ],
++ "dynamic_config" : 1,
++ "generated_by" : "Module::Build version 0.4003, CPAN::Meta::Converter version 2.112621",
++ "license" : [
++ "perl_5"
++ ],
++ "meta-spec" : {
++ "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
++ "version" : "2"
++ },
++ "name" : "App-ClusterSSH",
++ "prereqs" : {
++ "build" : {
++ "requires" : {
++ "File::Temp" : 0,
++ "File::Which" : 0,
++ "Readonly" : 0,
++ "Test::Differences" : 0,
++ "Test::DistManifest" : 0,
++ "Test::Pod" : 0,
++ "Test::Pod::Coverage" : 0,
++ "Test::Trap" : 0
++ }
++ },
++ "configure" : {
++ "requires" : {
++ "Module::Build" : 0
++ }
++ },
++ "runtime" : {
++ "requires" : {
++ "Exception::Class" : "1.31",
++ "Locale::Maketext" : 0,
++ "Tk" : "800.022",
++ "Try::Tiny" : 0,
++ "X11::Protocol" : "0.56",
++ "version" : 0
++ }
++ }
++ },
++ "provides" : {
++ "App::ClusterSSH" : {
++ "file" : "lib/App/ClusterSSH.pm",
++ "version" : "4.01_05"
++ },
++ "App::ClusterSSH::Base" : {
++ "file" : "lib/App/ClusterSSH/Base.pm",
++ "version" : "0.02"
++ },
++ "App::ClusterSSH::Cluster" : {
++ "file" : "lib/App/ClusterSSH/Cluster.pm",
++ "version" : "0.01"
++ },
++ "App::ClusterSSH::Config" : {
++ "file" : "lib/App/ClusterSSH/Config.pm",
++ "version" : "0.02"
++ },
++ "App::ClusterSSH::Helper" : {
++ "file" : "lib/App/ClusterSSH/Helper.pm",
++ "version" : "0.02"
++ },
++ "App::ClusterSSH::Host" : {
++ "file" : "lib/App/ClusterSSH/Host.pm",
++ "version" : "0.03"
++ },
++ "App::ClusterSSH::L10N" : {
++ "file" : "lib/App/ClusterSSH/L10N.pm",
++ "version" : 0
++ },
++ "App::ClusterSSH::L10N::en" : {
++ "file" : "lib/App/ClusterSSH/L10N/en.pm",
++ "version" : 0
++ }
++ },
++ "release_status" : "testing",
++ "resources" : {
++ "bugtracker" : {
++ "web" : "http://sourceforge.net/tracker/?group_id=89139"
++ },
++ "homepage" : "http://clusterssh.sourceforge.net/",
++ "license" : [
++ "http://dev.perl.org/licenses/"
++ ],
++ "x_Repository" : [
++ "http://clusterssh.git.sourceforge.net/",
++ "http://github.com/duncs/clusterssh"
++ ]
++ },
++ "version" : "4.01_05"
++}
+diff --git a/META.yml b/META.yml
+index bdf1020..7e93bb8 100644
+--- a/META.yml
++++ b/META.yml
+@@ -6,13 +6,15 @@ build_requires:
+ File::Temp: 0
+ File::Which: 0
+ Readonly: 0
++ Test::Differences: 0
+ Test::DistManifest: 0
+ Test::Pod: 0
+ Test::Pod::Coverage: 0
+ Test::Trap: 0
+ configure_requires:
+- Module::Build: 0.36
+-generated_by: 'Module::Build version 0.3603'
++ Module::Build: 0
++dynamic_config: 1
++generated_by: 'Module::Build version 0.4003, CPAN::Meta::Converter version 2.112621'
+ license: perl
+ meta-spec:
+ url: http://module-build.sourceforge.net/META-spec-v1.4.html
+@@ -21,7 +23,7 @@ name: App-ClusterSSH
+ provides:
+ App::ClusterSSH:
+ file: lib/App/ClusterSSH.pm
+- version: 4.01_01
++ version: 4.01_05
+ App::ClusterSSH::Base:
+ file: lib/App/ClusterSSH/Base.pm
+ version: 0.02
+@@ -30,17 +32,19 @@ provides:
+ version: 0.01
+ App::ClusterSSH::Config:
+ file: lib/App/ClusterSSH/Config.pm
+- version: 0.01
++ version: 0.02
+ App::ClusterSSH::Helper:
+ file: lib/App/ClusterSSH/Helper.pm
+- version: 0.01
++ version: 0.02
+ App::ClusterSSH::Host:
+ file: lib/App/ClusterSSH/Host.pm
+ version: 0.03
+ App::ClusterSSH::L10N:
+ file: lib/App/ClusterSSH/L10N.pm
++ version: 0
+ App::ClusterSSH::L10N::en:
+ file: lib/App/ClusterSSH/L10N/en.pm
++ version: 0
+ requires:
+ Exception::Class: 1.31
+ Locale::Maketext: 0
+@@ -52,7 +56,7 @@ resources:
+ bugtracker: http://sourceforge.net/tracker/?group_id=89139
+ homepage: http://clusterssh.sourceforge.net/
+ license: http://dev.perl.org/licenses/
+- repository:
++ x_Repository:
+ - http://clusterssh.git.sourceforge.net/
+ - http://github.com/duncs/clusterssh
+-version: 4.01_01
++version: 4.01_05
+diff --git a/Makefile.PL b/Makefile.PL
+index 4e126ef..059ea01 100644
+--- a/Makefile.PL
++++ b/Makefile.PL
+@@ -1,32 +1,33 @@
+-# Note: this file was auto-generated by Module::Build::Compat version 0.3603
++# Note: this file was auto-generated by Module::Build::Compat version 0.4003
+ use ExtUtils::MakeMaker;
+ WriteMakefile
+ (
+- 'NAME' => 'App::ClusterSSH',
+- 'VERSION_FROM' => 'lib/App/ClusterSSH.pm',
+- 'PREREQ_PM' => {
+- 'Exception::Class' => '1.31',
+- 'File::Temp' => 0,
+- 'File::Which' => 0,
+- 'Locale::Maketext' => 0,
+- 'Readonly' => 0,
+- 'Test::DistManifest' => 0,
+- 'Test::Pod' => 0,
+- 'Test::Pod::Coverage' => 0,
+- 'Test::Trap' => 0,
+- 'Tk' => '800.022',
+- 'Try::Tiny' => 0,
+- 'X11::Protocol' => '0.56',
+- 'version' => '0'
+- },
+- 'INSTALLDIRS' => 'site',
+- 'EXE_FILES' => [
+- 'bin/ccon',
+- 'bin/crsh',
+- 'bin/cscp',
+- 'bin/cssh',
+- 'bin/ctel'
+- ],
+- 'PL_FILES' => {}
+- )
++ 'NAME' => 'App::ClusterSSH',
++ 'VERSION_FROM' => 'lib/App/ClusterSSH.pm',
++ 'PREREQ_PM' => {
++ 'Exception::Class' => '1.31',
++ 'File::Temp' => 0,
++ 'File::Which' => 0,
++ 'Locale::Maketext' => 0,
++ 'Readonly' => 0,
++ 'Test::Differences' => 0,
++ 'Test::DistManifest' => 0,
++ 'Test::Pod' => 0,
++ 'Test::Pod::Coverage' => 0,
++ 'Test::Trap' => 0,
++ 'Tk' => '800.022',
++ 'Try::Tiny' => 0,
++ 'X11::Protocol' => '0.56',
++ 'version' => '0'
++ },
++ 'INSTALLDIRS' => 'site',
++ 'EXE_FILES' => [
++ 'bin/ccon',
++ 'bin/crsh',
++ 'bin/cscp',
++ 'bin/cssh',
++ 'bin/ctel'
++ ],
++ 'PL_FILES' => {}
++)
+ ;
+diff --git a/THANKS b/THANKS
+index 3af7cd0..bc2b3f7 100644
+--- a/THANKS
++++ b/THANKS
+@@ -40,3 +40,4 @@ Simon Fraser
+ Stefan Steiner
+ Ryan Brown
+ Brandon Perkins
++Oliver Meissner
+diff --git a/bin/cssh b/bin/cssh
+index 3a11dc1..6b95202 100755
+--- a/bin/cssh
++++ b/bin/cssh
+@@ -66,12 +66,12 @@ that host. Re-selecting it will plug it back in.
+ =item *
+
+ If your window manager menu bars are obscured by terminal windows see
+-the C<screen_reserve_XXXXX> options in the F<csshrc> file (see L</"FILES">).
++the C<screen_reserve_XXXXX> options in the F<$HOME/.clusterssh/config> file (see L</"FILES">).
+
+ =item *
+
+ If the terminals overlap too much see the C<terminal_reserve_XXXXX>
+-options in the F<csshrc> file (see L</"FILES">).
++options in the F<$HOME/.clusterssh/config> file (see L</"FILES">).
+
+ =item *
+
+@@ -115,7 +115,7 @@ be due to either the C<-xrm> terminal option which enables C<AllowSendEvents>
+ (some terminal do not require this option, other terminals have another
+ method for enabling it - see your terminal documention) or the
+ C<ConnectTimeout> ssh option (see the configuration option C<-o> or file
+-C<csshrc> below to resolve this).
++C<$HOME/.clusterssh/config> below to resolve this).
+
+ =back
+
+@@ -201,7 +201,7 @@ F<$HOME/.ssh/config>).
+ =item --output-config,-u
+
+ Output the current configuration in the same format used by the
+-F<$HOME/.csshrc> file.
++F<$HOME/.clusterssh/config> file.
+
+ =item --port,-p <port>
+
+@@ -224,6 +224,10 @@ Enable|Disable window tiling (overriding the config file)
+
+ Specify the initial part of the title used in the console and client windows
+
++=item --unique-servers,-m
++
++Connect to each host only once
++
+ =item --use_all_a_records,-A
+
+ If a hostname resolves to multiple IP addresses, toggle whether or not to
+@@ -255,7 +259,7 @@ on standard port (e.g not listening on port 22) and ssh_config cannot be used.
+ =item <tag> ...
+
+ Open a series of xterms defined by <tag> within either /etc/clusters or
+-F<$HOME/.csshrc> (see L</"FILES">).
++F<$HOME/.clusterssh/config> (see L</"FILES">).
+
+ Note: specifying a username on a cluster tag will override any usernames
+ defined in the cluster
+@@ -354,13 +358,13 @@ nested, but be aware of recursive tags which are not checked for.
+
+ Clusters may also be specified either directly (see C<clusters> configuration
+ options) or indirectly (see C<extra_cluster_file> configuration option)
+-in the users F<$HOME/.csshrc> file.
++in the users F<$HOME/.clusterssh/config> file.
+
+ NOTE: there is a special cluster tag called C<default> - any tags or hosts
+ included within this tag will be automatically opened if no other tags
+ are specified on the command line.
+
+-=item F</etc/csshrc> & F<$HOME/.csshrc>
++=item F</etc/csshrc> & F<$HOME/.clusterssh/config>
+
+ This file contains configuration overrides - the defaults are as marked.
+ Default options are overwritten first by the global file, and then by the
+@@ -377,10 +381,6 @@ should be written as
+
+ =over
+
+-=item always_tile = yes
+-
+-Setting to anything other than C<yes> does not perform window tiling (see also -G).
+-
+ =item auto_close = 5
+
+ Close terminal window after this many seconds. If set to 0 will instead wait
+@@ -472,10 +472,21 @@ program start
+ Default key sequence to paste text into the console window using the mouse.
+ See below notes on shortcuts.
+
++=item rsh = rsh
++
++=item ssh = ssh
++
++=item telnet = telnet
++
++Set the path to the specific binary to use for the communication method, else
++uses the first match found in $PATH
++
+ =item rsh_args = <blank>
+
+ =item ssh_args = "-x -o ConnectTimeout=10"
+
++=item telnet_args = <blank>
++
+ Sets any arguments to be used with the communication method (defaults to ssh
+ arguments).
+
+@@ -725,7 +736,7 @@ If you have issues running cssh, first try:
+ C<< cssh -e [user@]<hostname>[:port] >>
+
+ This performs two tests to confirm cssh is able to work properly with the
+-settings provided within the F<.csshrc> file (or internal defaults).
++settings provided within the F<$HOME/.clusterssh/config> file (or internal defaults).
+
+ 1. test the terminal window works with the options provided
+
+@@ -734,7 +745,7 @@ settings provided within the F<.csshrc> file (or internal defaults).
+ Configuration options to watch for in ssh are
+
+ - Doesnt understand "-o ConnectTimeout=10" - remove the option
+- in the F<.csshrc> file
++ in the F<$HOME/.clusterssh/config> file
+
+ - OpenSSH-3.8 using untrusted ssh tunnels - use "-Y" instead of "-X"
+ or use "ForwardX11Trusted yes' in ssh_config (if you change the
+@@ -751,7 +762,7 @@ C<< perl -MTk -e 'print $Tk::VERSION,$/' >>
+
+ C<< perl -MX11::Protocol -e 'print $X11::Protocol::VERSION,$/' >>
+
+-C<< cat /etc/csshrc $HOME/.csshrc >>
++C<< cat /etc/csshrc $HOME/.clusterssh/config >>
+
+ =item *
+
+diff --git a/lib/App/ClusterSSH.pm b/lib/App/ClusterSSH.pm
+index 793de94..ec568fc 100644
+--- a/lib/App/ClusterSSH.pm
++++ b/lib/App/ClusterSSH.pm
+@@ -3,7 +3,7 @@ package App::ClusterSSH;
+ use 5.008.004;
+ use warnings;
+ use strict;
+-use version; our $VERSION = version->new('4.01_01');
++use version; our $VERSION = version->new('4.01_05');
+
+ use Carp;
+
+@@ -116,12 +116,13 @@ my @options_spec = (
+ 'font|f=s',
+ 'list|L',
+ 'use_all_a_records|A',
++ 'unique-servers|m',
+ );
+ my %options;
+-my %windows; # hash for all window definitions
+-my %menus; # hash for all menu definitions
+-my @servers; # array of servers provided on cmdline
+-my %servers; # hash of server cx info
++my %windows; # hash for all window definitions
++my %menus; # hash for all menu definitions
++my @servers; # array of servers provided on cmdline
++my %servers; # hash of server cx info
+ my $xdisplay;
+ my %keyboardmap;
+ my $sysconfigdir = "/etc";
+@@ -277,53 +278,68 @@ sub load_keyboard_map() {
+
+ logmsg( 1, "Loading keymaps and keycodes" );
+
+- foreach ( 0 .. $#keyboard ) {
+- if ( defined $keyboard[$_][3] ) {
+- if ( defined( $keycodetosym{ $keyboard[$_][3] } ) ) {
+- $keyboardmap{ $keycodetosym{ $keyboard[$_][3] } }
+- = 'sa' . ( $_ + $min );
+- }
+- else {
+- logmsg( 2, "Unknown keycode ", $keyboard[$_][3] )
+- if ( $keyboard[$_][3] != 0 );
+- }
+- }
+- if ( defined $keyboard[$_][2] ) {
+- if ( defined( $keycodetosym{ $keyboard[$_][2] } ) ) {
+- $keyboardmap{ $keycodetosym{ $keyboard[$_][2] } }
+- = 'a' . ( $_ + $min );
+- }
+- else {
+- logmsg( 2, "Unknown keycode ", $keyboard[$_][2] )
+- if ( $keyboard[$_][2] != 0 );
+- }
+- }
+- if ( defined $keyboard[$_][1] ) {
+- if ( defined( $keycodetosym{ $keyboard[$_][1] } ) ) {
+- $keyboardmap{ $keycodetosym{ $keyboard[$_][1] } }
+- = 's' . ( $_ + $min );
+- }
+- else {
+- logmsg( 2, "Unknown keycode ", $keyboard[$_][1] )
+- if ( $keyboard[$_][1] != 0 );
+- }
+- }
+- if ( defined $keyboard[$_][0] ) {
+- if ( defined( $keycodetosym{ $keyboard[$_][0] } ) ) {
+- $keyboardmap{ $keycodetosym{ $keyboard[$_][0] } }
+- = 'n' . ( $_ + $min );
++ my %keyboard_modifier_priority = (
++ 'sa' => 3, # lowest
++ 'a' => 2,
++ 's' => 1,
++ 'n' => 0, # highest
++ );
++
++ my %keyboard_stringlike_modifiers = reverse %keyboard_modifier_priority;
++
++ # try to associate $keyboard=X11->GetKeyboardMapping table with X11::Keysyms
++ foreach my $i ( 0 .. $#keyboard ) {
++ for my $modifier ( 0 .. 3 ) {
++ if ( defined( $keycodetosym{ $keyboard[$i][$modifier] } ) ) {
++
++ # keyboard layout contains the keycode at $modifier level
++ if (defined(
++ $keyboardmap{ $keycodetosym{ $keyboard[$i][$modifier]
++ } }
++ )
++ )
++ {
++
++# we already have a mapping, let's see whether current one is better (lower shift state)
++ my ( $mod_code, $key_code )
++ = $keyboardmap{ $keycodetosym{ $keyboard[$i]
++ [$modifier] } } =~ /^(\D+)(\d+)$/;
++
++ # it is not easy to get around our own alien logic storing modifiers ;-)
++ if ( $modifier < $keyboard_modifier_priority{$mod_code} )
++ {
++
++ # YES! current keycode have priority over old one (phew!)
++ $keyboardmap{ $keycodetosym{ $keyboard[$i][$modifier]
++ } }
++ = $keyboard_stringlike_modifiers{$modifier}
++ . ( $i + $min );
++ }
++ }
++ else {
++
++ # we don't yet have a mapping... piece of cake!
++ $keyboardmap{ $keycodetosym{ $keyboard[$i][$modifier] } }
++ = $keyboard_stringlike_modifiers{$modifier}
++ . ( $i + $min );
++ }
+ }
+ else {
+- logmsg( 2, "Unknown keycode ", $keyboard[$_][0] )
+- if ( $keyboard[$_][0] != 0 );
++
++ # we didn't get the code from X11::Keysyms
++ if ( $keyboard[$i][$modifier] != 0 ) {
++
++ # ignore code=0
++ logmsg( 2, "Unknown keycode ", $keyboard[$i][$modifier] );
++ }
+ }
+ }
+-
+- # dont know these two key combs yet...
+- #$keyboardmap{ $keycodetosym { $keyboard[$_][4] } } = $_ + $min;
+- #$keyboardmap{ $keycodetosym { $keyboard[$_][5] } } = $_ + $min;
+ }
+
++ # dont know these two key combs yet...
++ #$keyboardmap{ $keycodetosym { $keyboard[$_][4] } } = $_ + $min;
++ #$keyboardmap{ $keycodetosym { $keyboard[$_][5] } } = $_ + $min;
++
+ #print "$_ => $keyboardmap{$_}\n" foreach(sort(keys(%keyboardmap)));
+ #print "keysymtocode: $keysymtocode{o}\n";
+ #die;
+@@ -383,8 +399,9 @@ sub resolve_names(@) {
+ if ( defined($hostobj) ) {
+ my @alladdrs = map { inet_ntoa($_) } @{ $hostobj->addr_list };
+ if ( $#alladdrs > 0 ) {
+- $self->cluster->register_tag($dirty, @alladdrs);
+- logmsg( 3, 'Expanded to ', $self->cluster->get_tag($dirty) );
++ $self->cluster->register_tag( $dirty, @alladdrs );
++ logmsg( 3, 'Expanded to ',
++ $self->cluster->get_tag($dirty) );
+ }
+ else {
+ logmsg( 3, 'Only one A record' );
+@@ -407,11 +424,22 @@ sub resolve_names(@) {
+ # now clean the array up
+ @servers = grep { $_ !~ m/^$/ } @servers;
+
++ if ($self->config->{unique_servers}) {
++ logmsg( 3, 'removing duplicate server names' );
++ @servers=remove_repeated_servers(@servers);
++ }
++
+ logmsg( 3, 'leaving with ', $_ ) foreach (@servers);
+ logmsg( 2, 'Resolving cluster names: completed' );
+ return (@servers);
+ }
+
++sub remove_repeated_servers {
++ my %all=();
++ @all{@_}=1;
++ return (keys %all);
++}
++
+ sub change_main_window_title() {
+ my ($self) = @_;
+ my $number = keys(%servers);
+@@ -588,7 +616,7 @@ sub open_client_windows(@) {
+ my $server_object = App::ClusterSSH::Host->parse_host_string($_);
+
+ my $username = $server_object->get_username();
+- $username = $self->config->{user} if ( $self->config->{user} );
++ $username = $self->config->{user} if ( !$username && $self->config->{user} );
+ my $port = $server_object->get_port();
+ $port = $self->config->{port} if ( $self->config->{port} );
+ my $server = $server_object->get_hostname();
+@@ -751,7 +779,7 @@ sub get_font_size() {
+
+ eval { (%font_info) = $xdisplay->QueryFont($font); }
+ || die( "Fatal: Unrecognised font used ($terminal_font).\n"
+- . "Please amend \$HOME/.csshrc with a valid font (see man page).\n"
++ . "Please amend \$HOME/.clusterssh/config with a valid font (see man page).\n"
+ );
+
+ $self->config->{internal_font_width}
+@@ -763,7 +791,7 @@ sub get_font_size() {
+ || !$self->config->{internal_font_height} )
+ {
+ die( "Fatal: Unrecognised font used ($terminal_font).\n"
+- . "Please amend \$HOME/.csshrc with a valid font (see man page).\n"
++ . "Please amend \$HOME/.clusterssh/config with a valid font (see man page).\n"
+ );
+ }
+
+@@ -1134,7 +1162,7 @@ sub add_host_by_name() {
+ $self->open_client_windows(@names);
+ }
+
+- if ( $menus{listbox}->curselection() ) {
++ if ( defined $menus{listbox} && $menus{listbox}->curselection() ) {
+ my @hosts = $menus{listbox}->get( $menus{listbox}->curselection() );
+ logmsg( 2, "host=", join( ' ', @hosts ) );
+ $self->open_client_windows( $self->resolve_names(@hosts) );
+@@ -1265,7 +1293,8 @@ sub setup_repeat() {
+ sub create_windows() {
+ my ($self) = @_;
+ logmsg( 2, "create_windows: started" );
+- $windows{main_window} = MainWindow->new( -title => "ClusterSSH" );
++ $windows{main_window}
++ = MainWindow->new( -title => "ClusterSSH", -class => 'cssh', );
+ $windows{main_window}->withdraw; # leave withdrawn until needed
+
+ if ( defined( $self->config->{console_position} )
+@@ -1279,6 +1308,7 @@ sub create_windows() {
+ -textvariable => \$menus{entrytext},
+ -insertborderwidth => 4,
+ -width => 25,
++ -class => 'cssh',
+ )->pack(
+ -fill => "x",
+ -expand => 1,
+@@ -1291,6 +1321,7 @@ sub create_windows() {
+ -height => $self->config->{history_height},
+ -state => 'normal',
+ -takefocus => 0,
++ -class => 'cssh',
+ );
+ $windows{history}->bindtags(undef);
+
+@@ -1353,6 +1384,7 @@ sub create_windows() {
+ -popover => $windows{main_window},
+ -overanchor => "c",
+ -popanchor => "c",
++ -class => 'cssh',
+ -font => [
+ -family => "interface system",
+ -size => 10,
+@@ -1367,6 +1399,7 @@ sub create_windows() {
+ -overanchor => "c",
+ -title => "Cssh Documentation",
+ -buttons => ['Close'],
++ -class => 'cssh',
+ );
+
+ my $manpage = `pod2text -l -q=\"\" $0 2>/dev/null`;
+@@ -1385,17 +1418,19 @@ sub create_windows() {
+ -title => "Add Host(s) or Cluster(s)",
+ -buttons => [ 'Add', 'Cancel' ],
+ -default_button => 'Add',
++ -class => 'cssh',
+ );
+
+ if ( $self->config->{max_addhost_menu_cluster_items}
+- && scalar $self->cluster->list_tags() )
++ && scalar $self->cluster->list_tags() )
+ {
+- if (scalar
+- scalar $self->cluster->list_tags() < $self->config->{max_addhost_menu_cluster_items} )
++ if (scalar scalar $self->cluster->list_tags()
++ < $self->config->{max_addhost_menu_cluster_items} )
+ {
+ $menus{listbox} = $windows{addhost}->Listbox(
+ -selectmode => 'extended',
+ -height => scalar $self->cluster->list_tags(),
++ -class => 'cssh',
+ )->pack();
+ }
+ else {
+@@ -1404,6 +1439,7 @@ sub create_windows() {
+ -scrollbars => 'e',
+ -selectmode => 'extended',
+ -height => $self->config->{max_addhost_menu_cluster_items},
++ -class => 'cssh',
+ )->pack();
+ }
+ $menus{listbox}->insert( 'end', sort $self->cluster->list_tags() );
+@@ -1415,6 +1451,7 @@ sub create_windows() {
+ -width => 20,
+ -label => 'Host',
+ -labelPack => [ -side => 'left', ],
++ -class => 'cssh',
+ )->pack( -side => 'left' );
+ logmsg( 2, "create_windows: completed" );
+
+@@ -1543,7 +1580,7 @@ sub key_event {
+
+ logmsg( 3, "key=:$key:" );
+ if ( $combo =~ /^$key$/ ) {
+- logmsg(3, "matched combo");
++ logmsg( 3, "matched combo" );
+ if ( $event eq "KeyRelease" ) {
+ logmsg( 2, "Received hotkey: $hotkey" );
+ send_text_to_all_servers('%s')
+@@ -1600,8 +1637,8 @@ sub key_event {
+ sub create_menubar() {
+ my ($self) = @_;
+ logmsg( 2, "create_menubar: started" );
+- $menus{bar} = $windows{main_window}->Menu;
+- $windows{main_window}->configure( -menu => $menus{bar} );
++ $menus{bar} = $windows{main_window}->Menu();
++ $windows{main_window}->configure( -menu => $menus{bar}, );
+
+ $menus{file} = $menus{bar}->cascade(
+ -label => 'File',
+@@ -1626,22 +1663,22 @@ sub create_menubar() {
+ -menuitems => [
+ [ "command",
+ "Retile Windows",
+- -command => sub{ $self->retile_hosts },
++ -command => sub { $self->retile_hosts },
+ -accelerator => $self->config->{key_retilehosts},
+ ],
+
+ # [ "command", "Capture Terminal", -command => \&capture_terminal, ],
+ [ "command",
+ "Toggle active state",
+- -command => sub{ $self->toggle_active_state() },
++ -command => sub { $self->toggle_active_state() },
+ ],
+ [ "command",
+ "Close inactive sessions",
+- -command => sub{ $self->close_inactive_sessions() },
++ -command => sub { $self->close_inactive_sessions() },
+ ],
+ [ "command",
+ "Add Host(s) or Cluster(s)",
+- -command => sub{ $self->add_host_by_name, },
++ -command => sub { $self->add_host_by_name, },
+ -accelerator => $self->config->{key_addhost},
+ ],
+ '',
+@@ -1667,7 +1704,8 @@ sub create_menubar() {
+ );
+
+ $windows{main_window}->bind( '<KeyPress>' => [ $self => 'key_event' ], );
+- $windows{main_window}->bind( '<KeyRelease>' => [ $self => 'key_event' ], );
++ $windows{main_window}
++ ->bind( '<KeyRelease>' => [ $self => 'key_event' ], );
+ logmsg( 2, "create_menubar: completed" );
+ }
+
+@@ -1797,12 +1835,15 @@ sub run {
+ }
+
+ if ( $options{action} ) {
+- $self->config->{command} = $options{action} ;
++ $self->config->{command} = $options{action};
+ }
+
++ $self->config->{unique_servers} = 1 if $options{'unique-servers'};
++
+ $self->config->{auto_quit} = "yes" if $options{autoquit};
+ $self->config->{auto_quit} = "no" if $options{'no-autoquit'};
+- $self->config->{auto_close} = $options{autoclose} if $options{'autoclose'};
++ $self->config->{auto_close} = $options{autoclose}
++ if defined $options{'autoclose'};
+
+ $self->config->{window_tiling} = "yes" if $options{tile};
+ $self->config->{window_tiling} = "no" if $options{'no-tile'};
+@@ -1815,7 +1856,7 @@ sub run {
+
+ $self->config->{terminal_font} = $options{font} if ( $options{font} );
+ $self->config->{terminal_args} = $options{'term-args'}
+- if ( $options{'term-args'} );
++ if ( $options{'term-args'} );
+ if ( $self->config->{terminal_args} =~ /-class (\w+)/ ) {
+ $self->config->{terminal_allow_send_events}
+ = "-xrm '$1.VT100.allowSendEvents:true'";
+@@ -1998,6 +2039,8 @@ the code until this time.
+
+ =item populate_send_menu_entries_from_xml
+
++=item remove_repeated_servers
++
+ =item resolve_names
+
+ =item retile_hosts
+diff --git a/lib/App/ClusterSSH/Base.pm b/lib/App/ClusterSSH/Base.pm
+index 95a92ed..e99cca4 100644
+--- a/lib/App/ClusterSSH/Base.pm
++++ b/lib/App/ClusterSSH/Base.pm
+@@ -157,7 +157,7 @@ sub set_config {
+
+ =head1 NAME
+
+-App::ClusterSSH::Base
++App::ClusterSSH::Base - Base object provding utility functions
+
+ =head1 SYNOPSIS
+
+diff --git a/lib/App/ClusterSSH/Cluster.pm b/lib/App/ClusterSSH/Cluster.pm
+index aabbbe8..3724b31 100644
+--- a/lib/App/ClusterSSH/Cluster.pm
++++ b/lib/App/ClusterSSH/Cluster.pm
+@@ -26,7 +26,7 @@ sub new {
+ sub get_clusters {
+ my ( $self, @files ) = @_;
+
+- for my $file ( '/etc/clusters', @files ) {
++ for my $file ( '/etc/clusters', $ENV{HOME}.'/.clusterssh/clusters',@files ) {
+ $self->debug(3, 'Loading in config from: ', $file);
+ $self->read_cluster_file($file);
+ }
+@@ -118,7 +118,7 @@ sub list_tags {
+
+ =head1 NAME
+
+-App::ClusterSSH::Cluster
++App::ClusterSSH::Cluster - Object representing cluster configuration
+
+ =head1 SYNOPSIS
+
+diff --git a/lib/App/ClusterSSH/Config.pm b/lib/App/ClusterSSH/Config.pm
+index dbe7c42..0c2fef0 100644
+--- a/lib/App/ClusterSSH/Config.pm
++++ b/lib/App/ClusterSSH/Config.pm
+@@ -4,20 +4,25 @@ use strict;
+ use warnings;
+
+ use version;
+-our $VERSION = version->new('0.01');
++our $VERSION = version->new('0.02');
+
+ use Carp;
+ use Try::Tiny;
+
+ use FindBin qw($Script);
++use File::Copy;
+
+ use base qw/ App::ClusterSSH::Base /;
+ use App::ClusterSSH::Cluster;
+
+ my $clusters;
+ my %old_clusters;
+-my @app_specific = (qw/ command title comms method ssh rsh telnet ccon /);
+-my %default_config = (
++my @app_specific = (qw/ command title comms method /);
++
++# list of config items to not write out when writing the default config
++my @ignore_default_config = (qw/ user /);
++
++my %default_config = (
+ terminal => "xterm",
+ terminal_args => "",
+ terminal_title_opt => "-T",
+@@ -54,9 +59,14 @@ my %default_config = (
+ terminal_decoration_height => 10,
+ terminal_decoration_width => 8,
+
+- rsh_args => "",
+- telnet_args => "",
+- ssh_args => "",
++ console => 'console',
++ console_args => '',
++ rsh => 'rsh',
++ rsh_args => "",
++ telnet => 'telnet',
++ telnet_args => "",
++ ssh => 'ssh',
++ ssh_args => "",
+
+ extra_cluster_file => "",
+
+@@ -76,6 +86,9 @@ my %default_config = (
+ use_all_a_records => 0,
+
+ send_menu_xml_file => $ENV{HOME} . '/.csshrc_send_menu',
++
++ # don't set username here as takes precendence over ssh config
++ user => '',
+ );
+
+ sub new {
+@@ -84,21 +97,19 @@ sub new {
+ my $self = $class->SUPER::new(%default_config);
+
+ ( my $comms = $Script ) =~ s/^c//;
+- $self->{comms} = $comms;
++
++ $comms = 'telnet' if ( $comms eq 'tel' );
++ $comms = 'console' if ( $comms eq 'con' );
++ $comms = 'ssh' if ( $comms eq 'lusterssh' );
+
+ # list of allowed comms methods
+- if ( 'ssh rsh telnet console' !~ m/\B$comms\B/ ) {
++ if ( 'ssh rsh telnet console' !~ m/\b$comms\b/ ) {
+ $self->{comms} = 'ssh';
+ }
+-
+- if ( $self->{comms}
+- && ( !$self->{ $self->{comms} } || !-e $self->{ $self->{comms} } ) )
+- {
+- $self->{ $self->{comms} } = $self->find_binary( $self->{comms} );
++ else {
++ $self->{comms} = $comms;
+ }
+
+- $self->{terminal} = $self->find_binary( $self->{terminal} );
+-
+ $self->{title} = uc($Script);
+
+ $clusters = App::ClusterSSH::Cluster->new();
+@@ -131,13 +142,39 @@ sub validate_args {
+ App::ClusterSSH::Exception::Config->throw(
+ unknown_config => \@unknown_config,
+ error => $self->loc(
+- 'Unknown configuration parameters: [_1]',
++ 'Unknown configuration parameters: [_1]' . $/,
+ join( ',', @unknown_config )
+ )
+ )
+ );
+ }
+
++ if ( !$self->{comms} ) {
++ croak(
++ App::ClusterSSH::Exception::Config->throw(
++ error => $self->loc( 'Invalid variable: comms' . $/ ),
++ ),
++ );
++ }
++
++ if ( !$self->{ $self->{comms} } ) {
++ croak(
++ App::ClusterSSH::Exception::Config->throw(
++ error => $self->loc(
++ 'Invalid variable: [_1]' . $/,
++ $self->{comms}
++ ),
++ ),
++ );
++ }
++
++ # # Don't search for the path to the binary - assume it is on the path
++ # # or defined correctly in the config.
++ # if( !-e $self->{ $self->{comms} } )
++ # {
++ # $self->{ $self->{comms} } = $self->find_binary( $self->{comms} );
++ # }
++
+ return $self;
+ }
+
+@@ -150,7 +187,8 @@ sub parse_config_file {
+ croak(
+ App::ClusterSSH::Exception::Config->throw(
+ error => $self->loc(
+- 'File [_1] does not exist or cannot be read', $config_file
++ 'File [_1] does not exist or cannot be read' . $/,
++ $config_file
+ ),
+ ),
+ );
+@@ -182,12 +220,13 @@ sub parse_config_file {
+ }
+ close(CFG);
+
+- # grab any c'lusters from the config before validating it
++ # grab any clusters from the config before validating it
+ if ( $read_config{clusters} ) {
+ $self->debug( 3, "Picked up clusters defined in $config_file" );
+ foreach my $cluster ( sort split / /, $read_config{clusters} ) {
+ if ( $read_config{$cluster} ) {
+- $clusters->register_tag( $cluster, $read_config{$cluster} );
++ $clusters->register_tag( $cluster,
++ split( / /, $read_config{$cluster} ) );
+ $old_clusters{$cluster} = $read_config{$cluster};
+ delete( $read_config{$cluster} );
+ }
+@@ -205,16 +244,6 @@ sub parse_config_file {
+ sub load_configs {
+ my ( $self, @configs ) = @_;
+
+- if ( -e $ENV{HOME} . '/.csshrc' ) {
+- warn(
+- $self->loc(
+- 'NOTICE: [_1] is no longer used - please see documentation and remove',
+- $ENV{HOME} . '/.csshrc'
+- ),
+- $/
+- );
+- }
+-
+ for my $config (
+ '/etc/csshrc',
+ $ENV{HOME} . '/.csshrc',
+@@ -248,6 +277,30 @@ sub load_configs {
+ sub write_user_config_file {
+ my ($self) = @_;
+
++ # attempt to move the old config file to one side
++ if ( -f "$ENV{HOME}/.csshrc" ) {
++ eval { move( "$ENV{HOME}/.csshrc", "$ENV{HOME}/.csshrc.DISABLED" ) };
++
++ if ($@) {
++ croak(
++ App::ClusterSSH::Exception::Config->throw(
++ error => $self->loc(
++ 'Unable to move [_1] to [_2]: [_3]' . $/,
++ '$HOME/.csshrc', '$HOME/.csshrc.DISABLED', $@
++ ),
++ )
++ );
++ }
++ else {
++ warn(
++ $self->loc(
++ 'Moved [_1] to [_2]' . $/, '$HOME/.csshrc',
++ '$HOME/.csshrc.DISABLED'
++ ),
++ );
++ }
++ }
++
+ return if ( -f "$ENV{HOME}/.clusterssh/config" );
+
+ if ( !-d "$ENV{HOME}/.clusterssh" ) {
+@@ -255,7 +308,7 @@ sub write_user_config_file {
+ croak(
+ App::ClusterSSH::Exception::Config->throw(
+ error => $self->loc(
+- 'Unable to create directory [_1]: [_2]',
++ 'Unable to create directory [_1]: [_2]' . $/,
+ '$HOME/.clusterssh', $!
+ ),
+ ),
+@@ -264,34 +317,58 @@ sub write_user_config_file {
+ }
+ }
+
++ # Debian #673507 - migrate clusters prior to writing ~/.clusterssh/config
++ # in order to update the extra_cluster_file property
++ if (%old_clusters) {
++ if ( open( my $fh, ">", "$ENV{HOME}/.clusterssh/clusters" ) ) {
++ print $fh '# '
++ . $self->loc('Tag definitions moved from old .csshrc file'),
++ $/;
++ foreach ( sort( keys(%old_clusters) ) ) {
++ print $fh $_, ' ', join( ' ', $old_clusters{$_} ), $/;
++ }
++ close($fh);
++ }
++ else {
++ croak(
++ App::ClusterSSH::Exception::Config->throw(
++ error => $self->loc(
++ 'Unable to write [_1]: [_2]' . $/,
++ '$HOME/.clusterssh/clusters',
++ $!
++ ),
++ ),
++ );
++ }
++ }
++
+ if ( open( CONFIG, ">", "$ENV{HOME}/.clusterssh/config" ) ) {
+ foreach ( sort( keys(%$self) ) ) {
+- print CONFIG "$_=$self->{$_}\n";
++ my $comment='';
++ if ( grep /$_/, @ignore_default_config ) {
++ $comment='#';
++ }
++ print CONFIG ${comment},$_,'=',$self->{$_},$/;
+ }
+ close(CONFIG);
++ warn(
++ $self->loc(
++ 'Created new configuration file within [_1]' . $/,
++ '$HOME/.clusterssh/'
++ )
++ );
+ }
+ else {
+ croak(
+ App::ClusterSSH::Exception::Config->throw(
+ error => $self->loc(
+- 'Unable to write default [_1]: [_2]',
+- '$HOME/.clusterssh/config',
+- $!
++ 'Unable to write default [_1]: [_2]' . $/,
++ '$HOME/.clusterssh/config', $!
+ ),
+ ),
+ );
+ }
+
+- return $self if ( !%old_clusters );
+-
+- if ( open( my $fh, ">", "$ENV{HOME}/.clusterssh/clusters" ) ) {
+- print $fh '# '
+- . $self->loc('Tag definitions moved from old .csshrc file'), $/;
+- foreach ( sort( keys(%old_clusters) ) ) {
+- print $fh $_, ' ', join( ' ', $old_clusters{$_} ), $/;
+- }
+- close($fh);
+- }
+ return $self;
+ }
+
+@@ -303,7 +380,7 @@ sub find_binary {
+ if ( !$binary ) {
+ croak(
+ App::ClusterSSH::Exception::Config->throw(
+- error => $self->loc('argument not provided'),
++ error => $self->loc('argument not provided') . $/,
+ ),
+ );
+ }
+@@ -355,7 +432,8 @@ sub find_binary {
+ croak(
+ App::ClusterSSH::Exception::Config->throw(
+ error => $self->loc(
+- '"[_1]" binary not found - please amend $PATH or the cssh config file',
++ '"[_1]" binary not found - please amend $PATH or the cssh config file'
++ . $/,
+ $binary
+ ),
+ ),
+@@ -373,10 +451,14 @@ sub dump {
+ print( '# Configuration dump produced by "cssh -u"', $/ );
+
+ foreach my $key ( sort keys %$self ) {
++ my $comment='';
+ if ( grep /$key/, @app_specific ) {
+ next;
+ }
+- print $key, '=', $self->{$key}, $/;
++ if ( grep /$key/, @ignore_default_config ) {
++ $comment='#';
++ }
++ print $comment, $key, '=', $self->{$key}, $/;
+ }
+
+ $self->exit if ( !$no_exit );
+@@ -396,7 +478,7 @@ sub dump {
+
+ =head1 NAME
+
+-ClusterSSH::Config
++ClusterSSH::Config - Object representing application configuration
+
+ =head1 SYNOPSIS
+
+diff --git a/lib/App/ClusterSSH/Helper.pm b/lib/App/ClusterSSH/Helper.pm
+index 07c5898..4289e77 100644
+--- a/lib/App/ClusterSSH/Helper.pm
++++ b/lib/App/ClusterSSH/Helper.pm
+@@ -4,7 +4,7 @@ use strict;
+ use warnings;
+
+ use version;
+-our $VERSION = version->new('0.01');
++our $VERSION = version->new('0.02');
+
+ use Carp;
+ use Try::Tiny;
+@@ -22,12 +22,12 @@ sub new {
+ sub script {
+ my ($self, $config ) = @_;
+
+- my $comms = $config->{comms};
+- my $comms_args = $config->{$comms.'_args'};
+- my $command = $config->{command};
++ my $comms = $config->{ $config->{comms} };
++ my $comms_args = $config->{ $config->{comms} . '_args'};
++ my $config_command = $config->{command};
+ my $autoclose = $config->{auto_close};
+
+- my $postcommand = $autoclose ? "echo Press RETURN to continue; read IGNORE" : "sleep $autoclose";
++ my $postcommand = $autoclose ? "echo Sleeping for $autoclose seconds; sleep $autoclose" : "echo Press RETURN to continue; read IGNORE"; # : "sleep $autoclose";
+
+ # # P = pipe file
+ # # s = server
+@@ -119,7 +119,10 @@ sub script {
+ \$command .= "\$svr";
+ }
+ }
+- \$command .= " \\\"$command\\\" ; $postcommand";
++ if("$config_command") {
++ \$command .= " \\\"$config_command\\\"";
++ }
++ \$command .= " ; $postcommand";
+ warn("Running:\$command\\n"); # for debug purposes
+ exec(\$command);
+ HERE
+@@ -145,7 +148,7 @@ sub script {
+
+ =head1 NAME
+
+-ClusterSSH::Helper
++ClusterSSH::Helper - Object representing helper script
+
+ =head1 SYNOPSIS
+
+diff --git a/lib/App/ClusterSSH/Host.pm b/lib/App/ClusterSSH/Host.pm
+index c9c00be..7d352e5 100644
+--- a/lib/App/ClusterSSH/Host.pm
++++ b/lib/App/ClusterSSH/Host.pm
+@@ -301,7 +301,7 @@ use overload (
+
+ =head1 NAME
+
+-ClusterSSH::Host
++ClusterSSH::Host - Object representing a host.
+
+ =head1 SYNOPSIS
+
+diff --git a/t/15config.t b/t/15config.t
+index 81d8f70..cce91a6 100644
+--- a/t/15config.t
++++ b/t/15config.t
+@@ -8,6 +8,7 @@ use Test::More;
+ use Test::Trap;
+ use File::Which qw(which);
+ use File::Temp qw(tempdir);
++use Test::Differences;
+
+ use Readonly;
+
+@@ -21,7 +22,7 @@ $config = App::ClusterSSH::Config->new();
+ isa_ok( $config, 'App::ClusterSSH::Config' );
+
+ Readonly::Hash my %default_config => {
+- terminal => "/usr/bin/xterm",
++ terminal => "xterm",
+ terminal_args => "",
+ terminal_title_opt => "-T",
+ terminal_colorize => 1,
+@@ -59,9 +60,14 @@ Readonly::Hash my %default_config => {
+
+ ssh => '/usr/bin/ssh',
+
+- rsh_args => "",
+- telnet_args => "",
+- ssh_args => "",
++ console => 'console',
++ console_args => '',
++ rsh => 'rsh',
++ rsh_args => "",
++ telnet => 'telnet',
++ telnet_args => "",
++ ssh => 'ssh',
++ ssh_args => "",
+
+ extra_cluster_file => "",
+
+@@ -72,8 +78,8 @@ Readonly::Hash my %default_config => {
+ history_height => 10,
+
+ command => q{},
+- title => q{15CONFIG.T},
+- comms => q{ssh},
++ title => q{15CONFIG.T},
++ comms => q{ssh},
+ max_host_menu_items => 30,
+
+ max_addhost_menu_cluster_items => 6,
+@@ -88,6 +94,7 @@ Readonly::Hash my %default_config => {
+ debug => 0,
+ lang => 'en',
+
++ user => '',
+ };
+ my %expected = %default_config;
+ is_deeply( $config, \%expected, 'default config is correct' );
+@@ -101,7 +108,7 @@ trap {
+ };
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ is( $trap->die,
+- 'Unknown configuration parameters: doesnt_exist,whoops',
++ 'Unknown configuration parameters: doesnt_exist,whoops' . $/,
+ 'got correct error message'
+ );
+ is_deeply(
+@@ -134,7 +141,7 @@ trap {
+ };
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ is( $trap->die,
+- "File $file does not exist or cannot be read",
++ "File $file does not exist or cannot be read" . $/,
+ 'got correct error message'
+ );
+
+@@ -166,7 +173,7 @@ trap {
+ is( $trap->leaveby, 'die', 'died ok' );
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ is( $trap->die,
+- 'Unknown configuration parameters: missing,rubbish',
++ 'Unknown configuration parameters: missing,rubbish' . $/,
+ 'die message correct'
+ );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+@@ -197,7 +204,7 @@ trap {
+ is( $trap->leaveby, 'die', 'died ok' );
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+-is( $trap->die, 'argument not provided', 'die message correct' );
++is( $trap->die, 'argument not provided' . $/, 'die message correct' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr, q{}, 'Expecting no STDERR' );
+@@ -210,7 +217,8 @@ is( $trap->leaveby, 'die', 'died ok' );
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->die,
+- '"missing" binary not found - please amend $PATH or the cssh config file',
++ '"missing" binary not found - please amend $PATH or the cssh config file'
++ . $/,
+ 'die message correct'
+ );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+@@ -241,11 +249,11 @@ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr, q{}, 'Expecting no STDERR' );
+ is_deeply( $config, \%expected, 'amended config is correct' );
+ is( $path, which('ls'), 'Found correct path to "ls"' );
+-is( $path, $newpath, 'No change made from find_binary');
++is( $path, $newpath, 'No change made from find_binary' );
+
+ # give false path to force another search
+ trap {
+- $newpath = $config->find_binary('/does/not/exist/'.$path);
++ $newpath = $config->find_binary( '/does/not/exist/' . $path );
+ };
+ is( $trap->leaveby, 'return', 'returned ok' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+@@ -254,7 +262,7 @@ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr, q{}, 'Expecting no STDERR' );
+ is_deeply( $config, \%expected, 'amended config is correct' );
+ is( $path, which('ls'), 'Found correct path to "ls"' );
+-is( $path, $newpath, 'No change made from find_binary');
++is( $path, $newpath, 'No change made from find_binary' );
+
+ note('Checks on loading configs');
+ note('empty dir');
+@@ -268,7 +276,10 @@ isa_ok( $config, "App::ClusterSSH::Config" );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->die, undef, 'die message correct' );
+ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+-is( $trap->stderr, q{}, 'Expecting no STDERR' );
++is( $trap->stderr,
++ 'Created new configuration file within $HOME/.clusterssh/' . $/,
++ 'Got correct STDERR output for .csshrc'
++);
+
+ #note(qx/ls -laR $ENV{HOME}/);
+ ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' );
+@@ -292,9 +303,9 @@ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->die, undef, 'die message correct' );
+ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr,
+- 'NOTICE: '
+- . $ENV{HOME}
+- . '/.csshrc is no longer used - please see documentation and remove'
++ 'Moved $HOME/.csshrc to $HOME/.csshrc.DISABLED'
++ . $/
++ . 'Created new configuration file within $HOME/.clusterssh/'
+ . $/,
+ 'Got correct STDERR output for .csshrc'
+ );
+@@ -303,6 +314,12 @@ ok( -f $ENV{HOME} . '/.clusterssh/config', '.clusterssh config file exists' );
+ is_deeply( $config, \%expected, 'amended config is correct' );
+
+ note('.csshrc warning and .clusterssh dir plus config');
++
++# need to recreate .csshrc as it was just moved
++open( $csshrc, '>', $ENV{HOME} . '/.csshrc' );
++print $csshrc 'auto_quit = no', $/;
++close($csshrc);
++$expected{auto_quit} = 'no';
+ open( $csshrc, '>', $ENV{HOME} . '/.clusterssh/config' );
+ print $csshrc 'window_tiling = no', $/;
+ close($csshrc);
+@@ -317,10 +334,7 @@ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->die, undef, 'die message correct' );
+ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr,
+- 'NOTICE: '
+- . $ENV{HOME}
+- . '/.csshrc is no longer used - please see documentation and remove'
+- . $/,
++ 'Moved $HOME/.csshrc to $HOME/.csshrc.DISABLED' . $/,
+ 'Got correct STDERR output for .csshrc'
+ );
+ ok( -d $ENV{HOME} . '/.clusterssh', '.clusterssh dir exists' );
+@@ -395,7 +409,7 @@ is( $trap->leaveby, 'die', 'died ok' );
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->die,
+- 'Unable to create directory $HOME/.clusterssh: File exists',
++ 'Unable to create directory $HOME/.clusterssh: File exists' . $/,
+ 'die message correct'
+ );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+@@ -414,7 +428,7 @@ is( $trap->leaveby, 'die', 'died ok' );
+ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Config' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->die,
+- 'Unable to write default $HOME/.clusterssh/config: Is a directory',
++ 'Unable to write default $HOME/.clusterssh/config: Is a directory' . $/,
+ 'die message correct'
+ );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+@@ -434,7 +448,7 @@ is( $trap->leaveby, 'return', 'died ok' );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr,
+- q{Unable to create directory $HOME/.clusterssh: File exists} . $/,
++ q{Unable to create directory $HOME/.clusterssh: File exists} . $/ . $/,
+ 'Expecting no STDERR'
+ );
+
+@@ -451,19 +465,23 @@ isa_ok( $config, "App::ClusterSSH::Config" );
+ isa_ok( $config, "App::ClusterSSH::Config" );
+ is( $trap->stdout, q{}, 'Expecting no STDOUT' );
+ is( $trap->stderr,
+- q{Unable to write default $HOME/.clusterssh/config: Is a directory} . $/,
++ q{Unable to write default $HOME/.clusterssh/config: Is a directory}
++ . $/
++ . $/,
+ 'Expecting no STDERR'
+ );
+
+ note('Checking dump');
+-$config = App::ClusterSSH::Config->new();
++$config = App::ClusterSSH::Config->new(
++ send_menu_xml_file => $ENV{HOME} . '/.csshrc_send_menu' );
+ trap {
+ $config->dump();
+ };
+-my $expected = <<'EOF';
+-# Configuration dump produced by "cssh -u"
++my $expected = qq{# Configuration dump produced by "cssh -u"
+ auto_close=5
+ auto_quit=yes
++console=console
++console_args=
+ console_position=
+ debug=0
+ extra_cluster_file=
+@@ -481,16 +499,19 @@ max_host_menu_items=30
+ menu_host_autotearoff=0
+ menu_send_autotearoff=0
+ mouse_paste=Button-2
++rsh=rsh
+ rsh_args=
+ screen_reserve_bottom=60
+ screen_reserve_left=0
+ screen_reserve_right=0
+ screen_reserve_top=0
+-send_menu_xml_file=/home/dferguson/.csshrc_send_menu
++send_menu_xml_file=} . $ENV{HOME} . qq{/.csshrc_send_menu
+ show_history=0
++ssh=ssh
+ ssh_args=
++telnet=telnet
+ telnet_args=
+-terminal=/usr/bin/xterm
++terminal=xterm
+ terminal_allow_send_events=-xrm '*.VT100.allowSendEvents:true'
+ terminal_args=
+ terminal_bg_style=dark
+@@ -507,12 +528,14 @@ terminal_title_opt=-T
+ unmap_on_redraw=no
+ use_all_a_records=0
+ use_hotkeys=yes
++#user=
+ window_tiling=yes
+ window_tiling_direction=right
+-EOF
++};
++
+ isa_ok( $config, "App::ClusterSSH::Config" );
+-is( $trap->die, undef, 'die message correct' );
+-is( $trap->stdout, $expected, 'Expecting no STDOUT' );
+-is( $trap->stderr, q{}, 'Expecting no STDERR' );
++is( $trap->die, undef, 'die message correct' );
++eq_or_diff( $trap->stdout, $expected, 'Expecting no STDOUT' );
++is( $trap->stderr, q{}, 'Expecting no STDERR' );
+
+ done_testing();
+diff --git a/t/30cluster.t b/t/30cluster.t
+index 487bbfc..1beadec 100644
+--- a/t/30cluster.t
++++ b/t/30cluster.t
+@@ -8,6 +8,7 @@ use Test::More;
+ use Test::Trap;
+ use File::Which qw(which);
+ use File::Temp qw(tempdir);
++use English '-no_match_vars';
+
+ use Readonly;
+
+@@ -27,24 +28,33 @@ $cluster1->register_tag( 'people', @expected );
+
+ my @got = $cluster2->get_tag('people');
+
+-is_deeply( \@got, \@expected,
+- 'Shared cluster object' );
++is_deeply( \@got, \@expected, 'Shared cluster object' );
+
+ # should pass without issue
+ trap {
+ $cluster1->read_cluster_file( $Bin . '/30cluster.doesnt exist' );
+ };
+-is( ! $trap, '', 'coped with missing file ok' );
++is( !$trap, '', 'coped with missing file ok' );
+ isa_ok( $cluster1, 'App::ClusterSSH::Cluster' );
+
+-my $no_read=$Bin . '/30cluster.cannot_read';
+-chmod 0000, $no_read;
+-trap {
+- $cluster1->read_cluster_file( $no_read );
+-};
+-chmod 0644, $no_read;
+-isa_ok( $trap->die, 'App::ClusterSSH::Exception::Cluster' );
+-is( $trap->die, "Unable to read file $no_read: Permission denied", 'Error on reading an existing file ok');
++# no point running this test as root since root cannot be blocked
++# from accessing the file
++if ( $EUID != 0 ) {
++ my $no_read = $Bin . '/30cluster.cannot_read';
++ chmod 0000, $no_read;
++ trap {
++ $cluster1->read_cluster_file($no_read);
++ };
++ chmod 0644, $no_read;
++ isa_ok( $trap->die, 'App::ClusterSSH::Exception::Cluster' );
++ is( $trap->die,
++ "Unable to read file $no_read: Permission denied",
++ 'Error on reading an existing file ok'
++ );
++}
++else {
++ pass('Cannot test for lack of read access when run as root');
++}
+
+ @expected = ('host1');
+ $cluster1->read_cluster_file( $Bin . '/30cluster.file1' );
+@@ -53,18 +63,15 @@ is_deeply( \@got, \@expected, 'read simple file OK' );
+
+ @expected = ('host1');
+ $cluster1->read_cluster_file( $Bin . '/30cluster.file2' );
+-@got=$cluster1->get_tag('tag1');
+-is_deeply( \@got,
+- \@expected, 'read more complex file OK' );
++@got = $cluster1->get_tag('tag1');
++is_deeply( \@got, \@expected, 'read more complex file OK' );
+
+ @expected = ('host2');
+-@got=$cluster1->get_tag('tag2');
+-is_deeply( \@got,
+- \@expected, 'read more complex file OK' );
++@got = $cluster1->get_tag('tag2');
++is_deeply( \@got, \@expected, 'read more complex file OK' );
+
+ @expected = ( 'host3', 'host4' );
+-@got=$cluster1->get_tag('tag3');
+-is_deeply( \@got,
+- \@expected, 'read more complex file OK' );
++@got = $cluster1->get_tag('tag3');
++is_deeply( \@got, \@expected, 'read more complex file OK' );
+
+ done_testing();
diff --git a/network/ClusterSSH/slack-desc b/network/ClusterSSH/slack-desc
new file mode 100644
index 0000000000..ea69925bbf
--- /dev/null
+++ b/network/ClusterSSH/slack-desc
@@ -0,0 +1,19 @@
+# HOW TO EDIT THIS FILE:
+# The "handy ruler" below makes it easier to edit a package description.
+# Line up the first '|' above the ':' following the base package name, and
+# the '|' on the right side marks the last column you can put a character in.
+# You must make exactly 11 lines for the formatting to be correct. It's also
+# customary to leave one space after the ':' except on otherwise blank lines.
+
+ |-----handy-ruler------------------------------------------------------|
+ClusterSSH: ClusterSSH (running multiple ssh clients at the same time in paralell)
+ClusterSSH:
+ClusterSSH: ClusterSSH is a tool for making the same change on multiple servers
+ClusterSSH: at the same time. The 'cssh' command opens an administration console
+ClusterSSH: and an xterm to all specified hosts. Any text typed into the
+ClusterSSH: administration console is replicated to all windows. All windows may
+ClusterSSH: also be typed into directly.
+ClusterSSH:
+ClusterSSH: Homepage: http://clusterssh.sourceforge.net
+ClusterSSH:
+ClusterSSH: