File Coverage

blib/lib/Citrix/SessionSet.pm
Criterion Covered Total %
statement 3 175 1.7
branch 0 68 0.0
condition 0 9 0.0
subroutine 1 16 6.2
pod 6 13 46.1
total 10 281 3.5


line stmt bran cond sub pod time code
1             package Citrix::SessionSet;
2 2     2   1866 use Data::Dumper;
  2         10446  
  2         4746  
3             #use strict;
4             #use warnings;
5              
6             our $VERSION = "0.25";
7              
8             # TODO: Allow loading sessions from perl cache file or DB for mock testing
9             # # http://support.citrix.com/proddocs/index.jsp?topic=/ps-unix/ps-unix-cmd-ref-commands-ctxquery.html
10              
11             =head1 NAME
12              
13             Citrix::SessionSet - Query UNIX Citrix Sessions from a Citrix Farm.
14              
15             =head1 DESCRIPTION
16              
17             Citrix::SessionSet Allows querying:
18              
19             =over 4
20              
21             =item 1) All sessions on farm (multiple, typically 2-8 hosts, by "farm context")
22              
23             =item 2) Sessions on a single host (by DNS hostname)
24              
25             =item 3) Sessions for an individual user (by username).
26              
27             =back
28              
29             Parses output from Citrix command line tools to Perl objects.
30             The module tries to do its best to deal with traditional problems
31             of sub-shell execution (command piping) and remote shelling (rsh).
32              
33             A UNIX Citrix "application" is usually a complete Desktop environment, but may
34             also be single app like X-Terminal, Mail Client or Word processor.
35              
36             =cut
37              
38              
39              
40              
41             # Use -f for short format opts -o For long fmt options
42             # See p. 234 in Citrix Guide
43             # d = 'DEVICE' (client)
44             # i = 'HOST:ID' (Combo of host+sessid)
45             # I = 'IDLE TIME'
46             # S = STATE
47             # u = USER
48             # x = X display number
49             # s = 'SERVER NAME'
50             # l = LOGON TIME
51             # p = APPLICATION NAME published app (APPID)
52              
53             =head1 CLASS VARIABLES
54              
55             =head2 $Citrix::SessionSet::ctxcols
56              
57             Column format string
58              
59             =head2 @Citrix::SessionSet::ctxattr
60              
61             Citrix Session set attributes (matching the letters in $ctxcols format string).
62             These turn into hash keys in the sessionset collection.
63              
64             =head2 $Citrix::SessionSet::debug
65              
66             Class level "global" debugging level (Notice that also instance has a debug flag). Set to true to
67             troubleshoot Citrix::SessionSet retrieval.
68              
69             =cut
70              
71              
72             # The path of Citrix Command line utilities.
73             # This is used as a path prefix for commands to eliminate runtime guesswork.
74             #OLD: our $Citrix::binpath = "/opt/CTXSmf/bin";
75             #
76             our $debug = 0;
77             # Default Col layout of ctxquery
78             our $ctxcols = "iSupd"; # t
79             # Arributes to use in the session collection (mapping to tab output format
80             # specifiers above). Notice col format specifiers in $ctxcols and this should match.
81             my @ctxattr = ('HOST_SID','STATE','USERNAME','APPID','DEVICE',); #'TYPE'
82             # OLD Unused: Legacy Default Citrix Query Timeout
83             #our $tout = 5;
84              
85             # Provide a better alias (to use in API)
86             *Citrix::SessionSet::usersessions = \&Citrix::SessionSet::mysess;
87              
88              
89             =head1 METHODS
90              
91             =head2 my $ss = Citrix::SessionSet->new($farmctx);
92              
93             Construct a new Citrix session collection.
94             Indicate Farm context of query by $fc (See L).
95             Return empty session set (to be queried later)
96              
97             =cut
98              
99             sub new {
100 0     0 1   my ($class, $fc) = @_;
101             #OLD:my $ss = [];
102 0 0         if (!%$fc) {print("Session::new() : NO FC");return(undef);}
  0            
  0            
103 0           my $ss = {'sarr' => [], 'fc' => $fc};
104 0           bless($ss, $class);
105 0           return($ss);
106             }
107              
108             =head2 $err = $ss->gethostsess('the-cx-host-67');
109              
110             Get all sessions for a single host (passed as $host) and load the sessions (adding them) into
111             session set instance.
112             Return 1 for errors, 0 on success.
113              
114             =cut
115             sub gethostsess {
116 0     0 1   my ($ss, $host) = @_;
117 0           my $sarr = $ss->getsessions();
118 0           my $fh;
119 0           my $ap = '';
120 0           my $fc = $ss->farmctx();
121 0           my $mh = $ss->getmh(); # $fc->masterhost();
122 0           my $ds = $fc->domainsuffix(); # OLD: {'ds'}
123 0           my $cnt = scalar(@$sarr);
124 0           my $usehost; # Final Host to use for query
125 0           my $tout = $Citrix::touts->{'host'}; # 10;
126 0           my @times = ();
127 0           my $trace = debug($ss);
128 0 0 0       if ($trace && $ENV{'HTTP_HOST'}) {print("
");} 
  0            
129             #if ($host =~ /\./) {die("Expecting bare hostname (got: $host)");}
130 0 0         if (!$host) {$usehost = $mh;$ap = ' -S';}
  0 0          
  0            
  0            
131             elsif ($ds) {$usehost = "$host.$ds";}
132 0           my $cmd = "rsh $usehost $Citrix::binpath/ctxquery -f $ctxcols $ap"; # -S
133             # Added loading of Net::Ping to circumvent
134 0           eval {require(Net::Ping);};
  0            
135 0 0         if ($@) {} # print("Dont have Net::Ping (risk hanging)");
136             else {
137 0           my $p = Net::Ping->new();
138 0 0         if ($p->ping($usehost)) {if ($trace) {print("$usehost is alive (reachable by PING).\n");}}
  0 0          
  0            
  0            
139             # Reuse $tout as state variable
140             else {$tout = 0;}
141 0           $p->close();
142 0 0         if (!$tout) {$ss->{'msg'} = "$usehost NOT Alive.\n";return(1);}
  0            
  0            
143             }
144 0 0         if ($trace) {print("Launch Query: $cmd\n");$times[0] = time();}
  0            
  0            
145 0           eval {
146             local $SIG{'ALRM'} = sub {
147 0     0     die("RSH Timeout ($usehost)\n");
148 0           die("Host '$usehost' was unable to return session within $tout\n");
149 0           };
150             #local $SIG{'CHLD'} = sub {die("Child ($usehost)\n");};
151 0           alarm($tout);
152 0 0         if ($trace) {print("Opening Pipe ...\n");}
  0            
153 0           my $ok = open($fh, "$cmd |");
154 0 0         if (!$ok) {die("Failed to open the pipe");}
  0            
155 0 0         if ($trace) {print("Opened: '$cmd' (as $< / $>)\n");}
  0            
156            
157             };
158             # Enforce reset in a place which is always visited
159 0           alarm(0);
160 0 0         if ($trace) {$times[1] = time();print("Done Trying (Success, ",($times[1]-$times[0])," s.)\n");}
  0            
  0            
161            
162 0 0         if ($trace) {print("Reset Timeout ($tout => 0)\n");}
  0            
163 0 0         if ($@) {$ss->{'msg'} = $@;return(2);}
  0            
  0            
164 0 0         if ($trace) {print("Parse Query (From: $fh)\n");}
  0            
165 0           parse($fh, $sarr, \@ctxattr);
166 0           my $cnt2 = scalar(@$sarr);
167 0 0         if ($host) {
168 0           my $cd = ($cnt2 - $cnt);
169 0           $ss->{'stat'}->{$host}->{'cnt'} = $cd;
170             #if (!$cd) {$ss->{'stat'}->{$host}->{'out'} = "$!";}
171             }
172 0 0 0       if ($trace && $ENV{'HTTP_HOST'}) {print("\n");}
  0            
173 0           return(0);
174             }
175              
176             =head2 $err = $ss->getsession('the-cx-host-67:5234');
177              
178             Get a session identified by $hostsess string (HOST:SESSID) from session set
179             (Involves sequential search within session set as sessions are not
180             indexed in current version).
181             The composite key of form "HOST:SESSID" is required, because session set may contain
182             sessions from multiple hosts (With single farm context though).
183             Return the single identified session (as hash) or undef if no session by SESSID
184             is found.
185              
186             =cut
187              
188             sub getsession {
189 0     0 1   my ($ss, $hostsess) = @_;
190 0           my $sarr = $ss->getsessions();
191 0           my (@s) = grep({$_->{'HOST_SID'} eq $hostsess} @$sarr);
  0            
192 0 0         if (@s < 1) {$ss->errstr("No Sessions for $hostsess'' ");return(undef);}
  0            
  0            
193 0 0         if (@s > 1) {$ss->errstr("Multiple session for Identified session '$hostsess'");return(undef);}
  0            
  0            
194             #ORG:return((@s == 1) ? $s[0] : undef);
195 0           return($s[0]);
196             }
197              
198             =head2 $err = $ss->mysess('joecitrix');
199              
200             Load sessions for single user (by username) into session set.
201             Usually loading takes place on an empty set to have truly the sessions for individual only.
202             This can be used to create "My Sessions" views, but this is just "Sessions for User by ID".
203             Return 1 (and up) for errors 0, for success.
204              
205             =cut
206             sub mysess {
207 0     0 1   my ($ss, $userid) = @_;
208 0           my $sarr = $ss->getsessions();
209 0           my $mh = $ss->getmh();
210 0 0         if (!$mh) {print("No Host to query from (master host for Farm)\n");return(1);}
  0            
  0            
211 0 0         if (!$userid) {print("Err: No User passed for getting sessions\n");return(1);}
  0            
  0            
212 0           my $cmd = "rsh $mh $Citrix::binpath/ctxquery -f $ctxcols -S user $userid"; # -S
213 0 0         if ($ss->debug()) {print("
$< / $>:  $cmd
\n");}
  0            
214 0           my $fh;
215 0           my $tout = $Citrix::touts->{'user'}; # 5;
216             local $SIG{'ALRM'} = sub {
217             # 'cx86-bh-1.bh' was not able respond within given timilimit
218             #die("Query Timeout after $tout s. (sig: '$_[0]', masterhost '$mh')");
219 0     0     die("Citrix server '$mh' (master) was not able respond within given timilimit ($tout s.)");
220 0           };
221 0           alarm($tout);
222 0           eval {
223 0           my $ok = open($fh, "$cmd |");
224 0 0         if (!$ok) {die("Failed to open the pipe");}
  0            
225             };
226 0           alarm(0);
227 0 0         if ($@) {print("Failed: $@");return(3);}
  0            
  0            
228 0           my $err = parse($fh, $sarr, \@ctxattr, 2);
229 0           return(0);
230             }
231              
232             # Access (Set/Get) all session from session set.
233             # Return session.
234             sub getsessions {
235 0     0 0   my ($ss, $set) = @_;
236 0 0         if (defined($set)) {$ss->{'sarr'} = $set;}
  0            
237 0           return($ss->{'sarr'});
238             }
239              
240             # Get the master host of farm context related to session set.
241             # Return master host name.
242             sub getmh {
243 0     0 0   my ($ss) = @_;
244 0           $ss->{'fc'}->masterhost(); # OLD: {'mh'}
245             }
246              
247             # Deprecated. See farmctx
248             #sub getfc {
249             # $_[0]->{'fc'};
250             #}
251              
252             # Get the complete Farm context node for current session set.
253             # Return Farm Context node.
254             sub farmctx {
255 0 0   0 0   if (@_ >= 2) {$_[0]->{'fc'} = $_[1];}
  0            
256 0           $_[0]->{'fc'};
257             }
258              
259             =head2 my $cnt = $ss->count();
260              
261             Accessor method to get the number of sessions stored in current session set.
262             Return the (integer) count.
263              
264             =cut
265             sub count {
266 0     0 1   my ($ss) = @_;
267 0           my $sarr = $ss->getsessions();
268 0           return(scalar(@$sarr));
269             }
270              
271             # See getsession()
272             sub getsessbyid {
273 0     0 0   my ($ss, $hostsess) = @_;
274 0           my $sarr = $ss->getsessions();
275 0           my (@s) = grep({$_->{'HOST_SID'} eq $hostsess} @$sarr);
  0            
276 0 0         if (scalar(@s) == 1) {return($s[0]);}
  0            
277 0           return(undef);
278             }
279              
280             # Toggle debug mode on in session set collection.
281             # This may be used in various contexts to produce more verbose output.
282             # Also class level (non-instance) debug flag is probed to find out
283             # the desired debug level.
284             # As a setter this can only affect the instance level debug setting.
285             sub debug {
286 0     0 1   my ($ss, $lv) = @_;
287 0 0         if (defined($lv)) {$ss->{'debug'} = $lv;}
  0            
288 0   0       return($ss->{'debug'} || $debug);
289             }
290             sub errstr {
291 0     0 0   my ($ss, $es) = @_;
292 0 0         if (defined($es)) {$ss->{'errstr'} = $es;}
  0            
293 0           $ss->{'errstr'};
294             }
295             # Parse output from Citrix Command pipe fiehandle $fh to $arr (which will be filled with hashes).
296             # Space delimited fields will be parsed into attributes @$attr in hashes.
297             # This parser is (sofar) applicable to all the possible outputs from
298             # Citrix commands returning tabular sets.
299             # For internal use only. Not part of exposed API.
300             # Return 0 (indicating success)
301             sub parse {
302 0     0 0   my ($fh, $arr, $attr, $scnt) = @_;
303 0 0         if (!$scnt) {$scnt = 0;} # To keep warnings silent
  0            
304             # Discard heading line
305 0           <$fh>;
306 0 0         if ($scnt > 1) {for (2..$scnt) {<$fh>}}
  0            
  0            
307 0           my $i = 0;
308 0           my $spcnt = scalar(@$attr);
309             # Consider error message from Citrix server
310 0           my $ere = qr/Session\s+info/; # not available
311 0           while (<$fh>) {
312             # Check early
313 0 0         if (/$ere/) {last;}
  0            
314 0           chomp();
315 0           s/^\s+//;
316 0 0         if (!$_) {next;}
  0            
317 0           my @a = split(/\s+/, $_, $spcnt);
318 0           my %h;
319 0           @h{@$attr} = @a;
320             # Separate this to a parser hook ?
321 0           @h{'HOST', 'SID'} = split(/:/, $h{'HOST_SID'});
322 0           $h{'APPID'} =~ s/^#//;
323             # Never care about STATE=listen,conn - Not here
324 0           push(@$arr, \%h);
325 0           $i++;
326             }
327 0           close($fh);
328 0           return(0);
329             }
330              
331              
332             # Internal: Extract Host Statistics Information from a session set (total number,
333             # distributions of various session states).
334             # Return Host Statistics (Hash of hashes).
335             sub hin {
336 0     0 0   my ($ss) = @_;
337 0           my $sarr = $ss->getsessions();
338 0           my %hosts;
339 0           map({
340 0           $hosts{$_->{'HOST'}}->{'tot'}++;
341 0           $hosts{$_->{'HOST'}}->{$_->{'STATE'}}++;
342             } @$sarr);
343             # Update Names into stats
344 0           for (keys(%hosts)) {$hosts{$_}->{'host'} = $_;}
  0            
345 0           return(\%hosts);
346             }
347              
348             1;
349              
350             #__END__
351             # =head2 NOTES
352             #