View | Details | Raw Unified | Return to bug 15338
Collapse All | Expand All

(-)a/C4/SIP/SIPServer.pm (-299 lines)
Lines 1-299 Link Here
1
#!/usr/bin/perl
2
package C4::SIP::SIPServer;
3
4
use strict;
5
use warnings;
6
use FindBin qw($Bin);
7
use lib "$Bin";
8
use Sys::Syslog qw(syslog);
9
use Net::Server::PreFork;
10
use IO::Socket::INET;
11
use Socket qw(:DEFAULT :crlf);
12
require UNIVERSAL::require;
13
14
use C4::SIP::Sip::Constants qw(:all);
15
use C4::SIP::Sip::Configuration;
16
use C4::SIP::Sip::Checksum qw(checksum verify_cksum);
17
use C4::SIP::Sip::MsgType qw( handle login_core );
18
use C4::SIP::Sip qw( read_SIP_packet );
19
20
use base qw(Net::Server::PreFork);
21
22
use constant LOG_SIP => "local6"; # Local alias for the logging facility
23
24
#
25
# Main	# not really, since package SIPServer
26
#
27
# FIXME: Is this a module or a script?  
28
# A script with no MAIN namespace?
29
# A module that takes command line args?
30
31
my %transports = (
32
    RAW    => \&raw_transport,
33
    telnet => \&telnet_transport,
34
);
35
36
#
37
# Read configuration
38
#
39
my $config = C4::SIP::Sip::Configuration->new( $ARGV[0] );
40
my @parms;
41
42
#
43
# Ports to bind
44
#
45
foreach my $svc (keys %{$config->{listeners}}) {
46
    push @parms, "port=" . $svc;
47
}
48
49
#
50
# Logging
51
#
52
# Log lines look like this:
53
# Jun 16 21:21:31 server08 steve_sip[19305]: ILS::Transaction::Checkout performing checkout...
54
# [  TIMESTAMP  ] [ HOST ] [ IDENT ]  PID  : Message...
55
#
56
# The IDENT is determined by config file 'server-params' arguments
57
58
59
#
60
# Server Management: set parameters for the Net::Server::PreFork
61
# module.  The module silently ignores parameters that it doesn't
62
# recognize, and complains about invalid values for parameters
63
# that it does.
64
#
65
if (defined($config->{'server-params'})) {
66
    while (my ($key, $val) = each %{$config->{'server-params'}}) {
67
		push @parms, $key . '=' . $val;
68
    }
69
}
70
71
72
#
73
# This is the main event.
74
__PACKAGE__ ->run(@parms);
75
76
#
77
# Child
78
#
79
80
# process_request is the callback used by Net::Server to handle
81
# an incoming connection request.
82
83
sub process_request {
84
    my $self = shift;
85
    my $service;
86
    my ($sockaddr, $port, $proto);
87
    my $transport;
88
89
    $self->{config} = $config;
90
91
    my $sockname = getsockname(STDIN);
92
93
    # Check if socket connection is IPv6 before resolving address
94
    my $family = Socket::sockaddr_family($sockname);
95
    if ($family == AF_INET6) {
96
      ($port, $sockaddr) = sockaddr_in6($sockname);
97
      $sockaddr = Socket::inet_ntop(AF_INET6, $sockaddr);
98
    } else {
99
      ($port, $sockaddr) = sockaddr_in($sockname);
100
      $sockaddr = inet_ntoa($sockaddr);
101
    }
102
    $proto = $self->{server}->{client}->NS_proto();
103
104
    $self->{service} = $config->find_service($sockaddr, $port, $proto);
105
106
    if (!defined($self->{service})) {
107
		syslog("LOG_ERR", "process_request: Unknown recognized server connection: %s:%s/%s", $sockaddr, $port, $proto);
108
		die "process_request: Bad server connection";
109
    }
110
111
    $transport = $transports{$self->{service}->{transport}};
112
113
    if (!defined($transport)) {
114
		syslog("LOG_WARNING", "Unknown transport '%s', dropping", $service->{transport});
115
		return;
116
    } else {
117
		&$transport($self);
118
    }
119
}
120
121
#
122
# Transports
123
#
124
125
sub raw_transport {
126
    my $self = shift;
127
    my ($input);
128
    my $service = $self->{service};
129
130
    while (!$self->{account}) {
131
    local $SIG{ALRM} = sub { die "raw_transport Timed Out!\n"; };
132
    syslog("LOG_DEBUG", "raw_transport: timeout is %d", $service->{timeout});
133
    $input = read_SIP_packet(*STDIN);
134
    if (!$input) {
135
        # EOF on the socket
136
        syslog("LOG_INFO", "raw_transport: shutting down: EOF during login");
137
        return;
138
    }
139
    $input =~ s/[\r\n]+$//sm;	# Strip off trailing line terminator(s)
140
    last if C4::SIP::Sip::MsgType::handle($input, $self, LOGIN);
141
    }
142
143
    syslog("LOG_DEBUG", "raw_transport: uname/inst: '%s/%s'",
144
	   $self->{account}->{id},
145
	   $self->{account}->{institution});
146
147
    $self->sip_protocol_loop();
148
    syslog("LOG_INFO", "raw_transport: shutting down");
149
}
150
151
sub get_clean_string {
152
	my $string = shift;
153
	if (defined $string) {
154
		syslog("LOG_DEBUG", "get_clean_string  pre-clean(length %s): %s", length($string), $string);
155
		chomp($string);
156
		$string =~ s/^[^A-z0-9]+//;
157
		$string =~ s/[^A-z0-9]+$//;
158
		syslog("LOG_DEBUG", "get_clean_string post-clean(length %s): %s", length($string), $string);
159
	} else {
160
		syslog("LOG_INFO", "get_clean_string called on undefined");
161
	}
162
	return $string;
163
}
164
165
sub get_clean_input {
166
	local $/ = "\012";
167
	my $in = <STDIN>;
168
	$in = get_clean_string($in);
169
	while (my $extra = <STDIN>){
170
		syslog("LOG_ERR", "get_clean_input got extra lines: %s", $extra);
171
	}
172
	return $in;
173
}
174
175
sub telnet_transport {
176
    my $self = shift;
177
    my ($uid, $pwd);
178
    my $strikes = 3;
179
    my $account = undef;
180
    my $input;
181
    my $config  = $self->{config};
182
	my $timeout = $self->{service}->{timeout} || $config->{timeout} || 30;
183
	syslog("LOG_DEBUG", "telnet_transport: timeout is %s", $timeout);
184
185
    eval {
186
	local $SIG{ALRM} = sub { die "telnet_transport: Timed Out ($timeout seconds)!\n"; };
187
	local $| = 1;			# Unbuffered output
188
	$/ = "\015";		# Internet Record Separator (lax version)
189
    # Until the terminal has logged in, we don't trust it
190
    # so use a timeout to protect ourselves from hanging.
191
192
	while ($strikes--) {
193
	    print "login: ";
194
		alarm $timeout;
195
		# $uid = &get_clean_input;
196
		$uid = <STDIN>;
197
	    print "password: ";
198
	    # $pwd = &get_clean_input || '';
199
		$pwd = <STDIN>;
200
		alarm 0;
201
202
		syslog("LOG_DEBUG", "telnet_transport 1: uid length %s, pwd length %s", length($uid), length($pwd));
203
		$uid = get_clean_string ($uid);
204
		$pwd = get_clean_string ($pwd);
205
		syslog("LOG_DEBUG", "telnet_transport 2: uid length %s, pwd length %s", length($uid), length($pwd));
206
207
	    if (exists ($config->{accounts}->{$uid})
208
		&& ($pwd eq $config->{accounts}->{$uid}->{password})) {
209
			$account = $config->{accounts}->{$uid};
210
			if ( C4::SIP::Sip::MsgType::login_core($self,$uid,$pwd) ) {
211
                last;
212
            }
213
	    }
214
		syslog("LOG_WARNING", "Invalid login attempt: '%s'", ($uid||''));
215
		print("Invalid login$CRLF");
216
	}
217
    }; # End of eval
218
219
    if ($@) {
220
		syslog("LOG_ERR", "telnet_transport: Login timed out");
221
		die "Telnet Login Timed out";
222
    } elsif (!defined($account)) {
223
		syslog("LOG_ERR", "telnet_transport: Login Failed");
224
		die "Login Failure";
225
    } else {
226
		print "Login OK.  Initiating SIP$CRLF";
227
    }
228
229
    $self->{account} = $account;
230
    syslog("LOG_DEBUG", "telnet_transport: uname/inst: '%s/%s'", $account->{id}, $account->{institution});
231
    $self->sip_protocol_loop();
232
    syslog("LOG_INFO", "telnet_transport: shutting down");
233
}
234
235
#
236
# The terminal has logged in, using either the SIP login process
237
# over a raw socket, or via the pseudo-unix login provided by the
238
# telnet transport.  From that point on, both the raw and the telnet
239
# processes are the same:
240
sub sip_protocol_loop {
241
	my $self = shift;
242
	my $service = $self->{service};
243
	my $config  = $self->{config};
244
    my $timeout = $self->{service}->{client_timeout} || $config->{client_timeout};
245
	my $input;
246
247
    # The spec says the first message will be:
248
	# 	SIP v1: SC_STATUS
249
	# 	SIP v2: LOGIN (or SC_STATUS via telnet?)
250
    # But it might be SC_REQUEST_RESEND.  As long as we get
251
    # SC_REQUEST_RESEND, we keep waiting.
252
253
    # Comprise reports that no other ILS actually enforces this
254
    # constraint, so we'll relax about it too.
255
    # Using the SIP "raw" login process, rather than telnet,
256
    # requires the LOGIN message and forces SIP 2.00.  In that
257
	# case, the LOGIN message has already been processed (above).
258
	# 
259
	# In short, we'll take any valid message here.
260
	#my $expect = SC_STATUS;
261
    local $SIG{ALRM} = sub { die "SIP Timed Out!\n"; } if $timeout;
262
    my $expect = '';
263
    while (1) {
264
        if ($timeout) {
265
            alarm $timeout;
266
        }
267
        $input = read_SIP_packet(*STDIN);
268
        unless ($input) {
269
            return;		# EOF
270
        }
271
		# begin input hacks ...  a cheap stand in for better Telnet layer
272
		$input =~ s/^[^A-z0-9]+//s;	# Kill leading bad characters... like Telnet handshakers
273
		$input =~ s/[^A-z0-9]+$//s;	# Same on the end, should get DOSsy ^M line-endings too.
274
		while (chomp($input)) {warn "Extra line ending on input";}
275
		unless ($input) {
276
            syslog("LOG_ERR", "sip_protocol_loop: empty input skipped");
277
            print("96$CR");
278
            next;
279
		}
280
		# end cheap input hacks
281
		my $status = handle($input, $self, $expect);
282
		if (!$status) {
283
			syslog("LOG_ERR", "sip_protocol_loop: failed to handle %s",substr($input,0,2));
284
		}
285
		next if $status eq REQUEST_ACS_RESEND;
286
		if ($expect && ($status ne $expect)) {
287
			# We received a non-"RESEND" that wasn't what we were expecting.
288
		    syslog("LOG_ERR", "sip_protocol_loop: expected %s, received %s, exiting", $expect, $input);
289
		}
290
		# We successfully received and processed what we were expecting
291
		$expect = '';
292
        if ($timeout ) {
293
            alarm 0;
294
        }
295
	}
296
}
297
298
1;
299
__END__
(-)a/misc/bin/SIPServer (+297 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
package SIPServer;
3
4
use strict;
5
use warnings;
6
use Sys::Syslog qw(syslog);
7
use Net::Server::PreFork;
8
use IO::Socket::INET;
9
use Socket qw(:DEFAULT :crlf);
10
require UNIVERSAL::require;
11
12
use C4::SIP::Sip::Constants qw(:all);
13
use C4::SIP::Sip::Configuration;
14
use C4::SIP::Sip::Checksum qw(checksum verify_cksum);
15
use C4::SIP::Sip::MsgType qw( handle login_core );
16
use C4::SIP::Sip qw( read_SIP_packet );
17
18
use base qw(Net::Server::PreFork);
19
20
use constant LOG_SIP => "local6"; # Local alias for the logging facility
21
22
#
23
# Main	# not really, since package SIPServer
24
#
25
# FIXME: Is this a module or a script?
26
# A script with no MAIN namespace?
27
# A module that takes command line args?
28
29
my %transports = (
30
    RAW    => \&raw_transport,
31
    telnet => \&telnet_transport,
32
);
33
34
#
35
# Read configuration
36
#
37
my $config = C4::SIP::Sip::Configuration->new( $ARGV[0] );
38
my @parms;
39
40
#
41
# Ports to bind
42
#
43
foreach my $svc (keys %{$config->{listeners}}) {
44
    push @parms, "port=" . $svc;
45
}
46
47
#
48
# Logging
49
#
50
# Log lines look like this:
51
# Jun 16 21:21:31 server08 steve_sip[19305]: ILS::Transaction::Checkout performing checkout...
52
# [  TIMESTAMP  ] [ HOST ] [ IDENT ]  PID  : Message...
53
#
54
# The IDENT is determined by config file 'server-params' arguments
55
56
57
#
58
# Server Management: set parameters for the Net::Server::PreFork
59
# module.  The module silently ignores parameters that it doesn't
60
# recognize, and complains about invalid values for parameters
61
# that it does.
62
#
63
if (defined($config->{'server-params'})) {
64
    while (my ($key, $val) = each %{$config->{'server-params'}}) {
65
		push @parms, $key . '=' . $val;
66
    }
67
}
68
69
70
#
71
# This is the main event.
72
__PACKAGE__ ->run(@parms);
73
74
#
75
# Child
76
#
77
78
# process_request is the callback used by Net::Server to handle
79
# an incoming connection request.
80
81
sub process_request {
82
    my $self = shift;
83
    my $service;
84
    my ($sockaddr, $port, $proto);
85
    my $transport;
86
87
    $self->{config} = $config;
88
89
    my $sockname = getsockname(STDIN);
90
91
    # Check if socket connection is IPv6 before resolving address
92
    my $family = Socket::sockaddr_family($sockname);
93
    if ($family == AF_INET6) {
94
      ($port, $sockaddr) = sockaddr_in6($sockname);
95
      $sockaddr = Socket::inet_ntop(AF_INET6, $sockaddr);
96
    } else {
97
      ($port, $sockaddr) = sockaddr_in($sockname);
98
      $sockaddr = inet_ntoa($sockaddr);
99
    }
100
    $proto = $self->{server}->{client}->NS_proto();
101
102
    $self->{service} = $config->find_service($sockaddr, $port, $proto);
103
104
    if (!defined($self->{service})) {
105
		syslog("LOG_ERR", "process_request: Unknown recognized server connection: %s:%s/%s", $sockaddr, $port, $proto);
106
		die "process_request: Bad server connection";
107
    }
108
109
    $transport = $transports{$self->{service}->{transport}};
110
111
    if (!defined($transport)) {
112
		syslog("LOG_WARNING", "Unknown transport '%s', dropping", $service->{transport});
113
		return;
114
    } else {
115
		&$transport($self);
116
    }
117
}
118
119
#
120
# Transports
121
#
122
123
sub raw_transport {
124
    my $self = shift;
125
    my ($input);
126
    my $service = $self->{service};
127
128
    while (!$self->{account}) {
129
    local $SIG{ALRM} = sub { die "raw_transport Timed Out!\n"; };
130
    syslog("LOG_DEBUG", "raw_transport: timeout is %d", $service->{timeout});
131
    $input = read_SIP_packet(*STDIN);
132
    if (!$input) {
133
        # EOF on the socket
134
        syslog("LOG_INFO", "raw_transport: shutting down: EOF during login");
135
        return;
136
    }
137
    $input =~ s/[\r\n]+$//sm;	# Strip off trailing line terminator(s)
138
    last if C4::SIP::Sip::MsgType::handle($input, $self, LOGIN);
139
    }
140
141
    syslog("LOG_DEBUG", "raw_transport: uname/inst: '%s/%s'",
142
	   $self->{account}->{id},
143
	   $self->{account}->{institution});
144
145
    $self->sip_protocol_loop();
146
    syslog("LOG_INFO", "raw_transport: shutting down");
147
}
148
149
sub get_clean_string {
150
	my $string = shift;
151
	if (defined $string) {
152
		syslog("LOG_DEBUG", "get_clean_string  pre-clean(length %s): %s", length($string), $string);
153
		chomp($string);
154
		$string =~ s/^[^A-z0-9]+//;
155
		$string =~ s/[^A-z0-9]+$//;
156
		syslog("LOG_DEBUG", "get_clean_string post-clean(length %s): %s", length($string), $string);
157
	} else {
158
		syslog("LOG_INFO", "get_clean_string called on undefined");
159
	}
160
	return $string;
161
}
162
163
sub get_clean_input {
164
	local $/ = "\012";
165
	my $in = <STDIN>;
166
	$in = get_clean_string($in);
167
	while (my $extra = <STDIN>){
168
		syslog("LOG_ERR", "get_clean_input got extra lines: %s", $extra);
169
	}
170
	return $in;
171
}
172
173
sub telnet_transport {
174
    my $self = shift;
175
    my ($uid, $pwd);
176
    my $strikes = 3;
177
    my $account = undef;
178
    my $input;
179
    my $config  = $self->{config};
180
	my $timeout = $self->{service}->{timeout} || $config->{timeout} || 30;
181
	syslog("LOG_DEBUG", "telnet_transport: timeout is %s", $timeout);
182
183
    eval {
184
	local $SIG{ALRM} = sub { die "telnet_transport: Timed Out ($timeout seconds)!\n"; };
185
	local $| = 1;			# Unbuffered output
186
	$/ = "\015";		# Internet Record Separator (lax version)
187
    # Until the terminal has logged in, we don't trust it
188
    # so use a timeout to protect ourselves from hanging.
189
190
	while ($strikes--) {
191
	    print "login: ";
192
		alarm $timeout;
193
		# $uid = &get_clean_input;
194
		$uid = <STDIN>;
195
	    print "password: ";
196
	    # $pwd = &get_clean_input || '';
197
		$pwd = <STDIN>;
198
		alarm 0;
199
200
		syslog("LOG_DEBUG", "telnet_transport 1: uid length %s, pwd length %s", length($uid), length($pwd));
201
		$uid = get_clean_string ($uid);
202
		$pwd = get_clean_string ($pwd);
203
		syslog("LOG_DEBUG", "telnet_transport 2: uid length %s, pwd length %s", length($uid), length($pwd));
204
205
	    if (exists ($config->{accounts}->{$uid})
206
		&& ($pwd eq $config->{accounts}->{$uid}->{password})) {
207
			$account = $config->{accounts}->{$uid};
208
			if ( C4::SIP::Sip::MsgType::login_core($self,$uid,$pwd) ) {
209
                last;
210
            }
211
	    }
212
		syslog("LOG_WARNING", "Invalid login attempt: '%s'", ($uid||''));
213
		print("Invalid login$CRLF");
214
	}
215
    }; # End of eval
216
217
    if ($@) {
218
		syslog("LOG_ERR", "telnet_transport: Login timed out");
219
		die "Telnet Login Timed out";
220
    } elsif (!defined($account)) {
221
		syslog("LOG_ERR", "telnet_transport: Login Failed");
222
		die "Login Failure";
223
    } else {
224
		print "Login OK.  Initiating SIP$CRLF";
225
    }
226
227
    $self->{account} = $account;
228
    syslog("LOG_DEBUG", "telnet_transport: uname/inst: '%s/%s'", $account->{id}, $account->{institution});
229
    $self->sip_protocol_loop();
230
    syslog("LOG_INFO", "telnet_transport: shutting down");
231
}
232
233
#
234
# The terminal has logged in, using either the SIP login process
235
# over a raw socket, or via the pseudo-unix login provided by the
236
# telnet transport.  From that point on, both the raw and the telnet
237
# processes are the same:
238
sub sip_protocol_loop {
239
	my $self = shift;
240
	my $service = $self->{service};
241
	my $config  = $self->{config};
242
    my $timeout = $self->{service}->{client_timeout} || $config->{client_timeout};
243
	my $input;
244
245
    # The spec says the first message will be:
246
	# 	SIP v1: SC_STATUS
247
	# 	SIP v2: LOGIN (or SC_STATUS via telnet?)
248
    # But it might be SC_REQUEST_RESEND.  As long as we get
249
    # SC_REQUEST_RESEND, we keep waiting.
250
251
    # Comprise reports that no other ILS actually enforces this
252
    # constraint, so we'll relax about it too.
253
    # Using the SIP "raw" login process, rather than telnet,
254
    # requires the LOGIN message and forces SIP 2.00.  In that
255
	# case, the LOGIN message has already been processed (above).
256
	#
257
	# In short, we'll take any valid message here.
258
	#my $expect = SC_STATUS;
259
    local $SIG{ALRM} = sub { die "SIP Timed Out!\n"; } if $timeout;
260
    my $expect = '';
261
    while (1) {
262
        if ($timeout) {
263
            alarm $timeout;
264
        }
265
        $input = read_SIP_packet(*STDIN);
266
        unless ($input) {
267
            return;		# EOF
268
        }
269
		# begin input hacks ...  a cheap stand in for better Telnet layer
270
		$input =~ s/^[^A-z0-9]+//s;	# Kill leading bad characters... like Telnet handshakers
271
		$input =~ s/[^A-z0-9]+$//s;	# Same on the end, should get DOSsy ^M line-endings too.
272
		while (chomp($input)) {warn "Extra line ending on input";}
273
		unless ($input) {
274
            syslog("LOG_ERR", "sip_protocol_loop: empty input skipped");
275
            print("96$CR");
276
            next;
277
		}
278
		# end cheap input hacks
279
		my $status = handle($input, $self, $expect);
280
		if (!$status) {
281
			syslog("LOG_ERR", "sip_protocol_loop: failed to handle %s",substr($input,0,2));
282
		}
283
		next if $status eq REQUEST_ACS_RESEND;
284
		if ($expect && ($status ne $expect)) {
285
			# We received a non-"RESEND" that wasn't what we were expecting.
286
		    syslog("LOG_ERR", "sip_protocol_loop: expected %s, received %s, exiting", $expect, $input);
287
		}
288
		# We successfully received and processed what we were expecting
289
		$expect = '';
290
        if ($timeout ) {
291
            alarm 0;
292
        }
293
	}
294
}
295
296
1;
297
__END__
(-)a/misc/bin/sip_run.sh (-7 / +5 lines)
Lines 16-21 Link Here
16
#   sip_run.sh ~/my_sip/SIPconfig.xml sip_out.log sip_err.log
16
#   sip_run.sh ~/my_sip/SIPconfig.xml sip_out.log sip_err.log
17
17
18
18
19
# check ENV variables defined
19
for x in HOME PERL5LIB KOHA_CONF ; do
20
for x in HOME PERL5LIB KOHA_CONF ; do
20
	echo $x=${!x}
21
	echo $x=${!x}
21
	if [ -z ${!x} ] ; then 
22
	if [ -z ${!x} ] ; then 
Lines 24-34 for x in HOME PERL5LIB KOHA_CONF ; do Link Here
24
	fi;
25
	fi;
25
done;
26
done;
26
unset x;
27
unset x;
27
# you should hard code this if you have multiple directories
28
# get the bin directory from the location of this script
28
# in your PERL5LIB
29
BINDIR="$( cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd)"
29
PERL_MODULE_DIR=$PERL5LIB
30
cd $PERL_MODULE_DIR/C4/SIP;
31
echo;
32
30
33
sipconfig=${1};
31
sipconfig=${1};
34
outfile=${2:-$HOME/sip.out};
32
outfile=${2:-$HOME/sip.out};
Lines 37-44 errfile=${3:-$HOME/sip.err}; Link Here
37
if [ $sipconfig ]; then
35
if [ $sipconfig ]; then
38
	echo "Running with config file located in $sipconfig" ;
36
	echo "Running with config file located in $sipconfig" ;
39
	echo "Calling (backgrounded):";
37
	echo "Calling (backgrounded):";
40
    echo "perl ./SIPServer.pm $sipconfig >>$outfile 2>>$errfile";
38
    echo "perl ./SIPServer $sipconfig >>$outfile 2>>$errfile";
41
    perl ./SIPServer.pm $sipconfig >>$outfile 2>>$errfile &
39
    $BINDIR/SIPServer $sipconfig >>$outfile 2>>$errfile &
42
40
43
else
41
else
44
	echo "Please specify a config file and try again."
42
	echo "Please specify a config file and try again."
(-)a/misc/bin/sip_shutdown.sh (-3 lines)
Lines 1-7 Link Here
1
#!/bin/bash
1
#!/bin/bash
2
2
3
. $HOME/.bash_profile
4
5
# this is brittle: the primary server must have the lowest PPID
3
# this is brittle: the primary server must have the lowest PPID
6
# this is brittle: ps behavior is very platform-specific, only tested on Debian Etch
4
# this is brittle: ps behavior is very platform-specific, only tested on Debian Etch
7
5
8
- 

Return to bug 15338