Commit | Line | Data |
0baf5fac |
1 | package FCGI::ProcManager; |
2 | |
3 | # Copyright (c) 2000, FundsXpress Financial Network, Inc. |
4 | # This library is free software released under the GNU Lesser General |
5 | # Public License, Version 2.1. Please read the important licensing and |
6 | # disclaimer information included below. |
7 | |
c2bbadb3 |
8 | # $Id: ProcManager.pm,v 1.17 2001/02/09 16:15:47 muaddie Exp $ |
0baf5fac |
9 | |
10 | use strict; |
4ceac1a1 |
11 | use Exporter; |
4ceac1a1 |
12 | |
518709ed |
13 | use vars qw($VERSION @ISA @EXPORT_OK %EXPORT_TAGS $Q); |
0baf5fac |
14 | BEGIN { |
c2bbadb3 |
15 | $VERSION = '0.16'; |
4ceac1a1 |
16 | @ISA = qw(Exporter); |
c2bbadb3 |
17 | @EXPORT_OK = qw(pm_manage pm_die pm_wait |
4ceac1a1 |
18 | pm_write_pid_file pm_remove_pid_file |
c146b63d |
19 | pm_pre_dispatch pm_post_dispatch |
518709ed |
20 | pm_change_process_name pm_received_signal pm_parameter |
21 | pm_warn pm_notify pm_abort pm_exit); |
4ceac1a1 |
22 | $EXPORT_TAGS{all} = \@EXPORT_OK; |
23 | $FCGI::ProcManager::Default = 'FCGI::ProcManager'; |
0baf5fac |
24 | } |
25 | |
26 | =head1 NAME |
27 | |
28 | FCGI::ProcManager - functions for managing FastCGI applications. |
29 | |
30 | =head1 SYNOPSIS |
31 | |
518709ed |
32 | { |
33 | # In Object-oriented style. |
0baf5fac |
34 | use CGI::Fast; |
35 | use FCGI::ProcManager; |
c2bbadb3 |
36 | my $proc_manager = FCGI::ProcManager->new({ |
37 | n_processes => 10 |
38 | }); |
518709ed |
39 | $proc_manager->pm_manage(); |
0baf5fac |
40 | while (my $cgi = CGI::Fast->new()) { |
518709ed |
41 | $proc_manager->pm_pre_dispatch(); |
42 | # ... handle the request here ... |
43 | $proc_manager->pm_post_dispatch(); |
44 | } |
45 | |
46 | # This style is also supported: |
47 | use CGI::Fast; |
c2bbadb3 |
48 | use FCGI::ProcManager qw(pm_manage pm_pre_dispatch |
49 | pm_post_dispatch); |
518709ed |
50 | pm_manage( n_processes => 10 ); |
51 | while (my $cgi = CGI::Fast->new()) { |
52 | pm_pre_dispatch(); |
0baf5fac |
53 | #... |
518709ed |
54 | pm_post_dispatch(); |
55 | } |
0baf5fac |
56 | |
57 | =head1 DESCRIPTION |
58 | |
518709ed |
59 | FCGI::ProcManager is used to serve as a FastCGI process manager. By |
60 | re-implementing it in perl, developers can more finely tune performance in |
61 | their web applications, and can take advantage of copy-on-write semantics |
62 | prevalent in UNIX kernel process management. The process manager should |
63 | be invoked before the caller''s request loop |
64 | |
65 | The primary routine, C<pm_manage>, enters a loop in which it maintains a |
66 | number of FastCGI servers (via fork(2)), and which reaps those servers |
67 | when they die (via wait(2)). |
68 | |
69 | C<pm_manage> provides too hooks: |
70 | |
71 | C<managing_init> - called just before the manager enters the manager loop. |
72 | C<handling_init> - called just before a server is returns from C<pm_manage> |
73 | |
74 | It is necessary for the caller, when implementing its request loop, to |
75 | insert a call to C<pm_pre_dispatch> at the top of the loop, and then |
76 | 7C<pm_post_dispatch> at the end of the loop. |
77 | |
0baf5fac |
78 | |
79 | =head1 METHODS |
80 | |
81 | =head2 new |
82 | |
c2bbadb3 |
83 | class or instance |
84 | (ProcManager) new([hash parameters]) |
85 | |
86 | Constructs a new process manager. Takes an option has of initial parameter |
87 | values, and assigns these to the constructed object HASH, overriding any |
88 | default values. The default parameter values currently are: |
89 | |
90 | role => manager |
91 | start_delay => 0 |
92 | die_timeout => 60 |
93 | |
0baf5fac |
94 | =cut |
95 | |
96 | sub new { |
97 | my ($proto,$init) = @_; |
98 | |
518709ed |
99 | my $this = { |
100 | role => "manager", |
101 | start_delay => 0, |
102 | die_timeout => 60 |
103 | }; |
0baf5fac |
104 | $init and %$this = %$init; |
4ceac1a1 |
105 | |
106 | bless $this, ref($proto)||$proto; |
0baf5fac |
107 | |
108 | $this->{PIDS} = {}; |
109 | |
110 | return $this; |
111 | } |
112 | |
518709ed |
113 | =head1 Manager methods |
4ceac1a1 |
114 | |
115 | =head2 pm_manage |
0baf5fac |
116 | |
c2bbadb3 |
117 | instance or export |
118 | (int) pm_manage([hash parameters]) |
0baf5fac |
119 | |
120 | DESCRIPTION: |
121 | |
c2bbadb3 |
122 | When this is called by a FastCGI script to manage application servers. It |
123 | defines a sequence of instructions for a process to enter this method and |
124 | begin forking off and managing those handlers, and it defines a sequence of |
125 | instructions to intialize those handlers. |
126 | |
127 | If n_processes < 1, the managing section is subverted, and only the |
128 | handling sequence is executed. |
129 | |
130 | Either returns the return value of pm_die() and/or pm_abort() (which will |
131 | not ever return in general), or returns 1 to the calling script to begin |
132 | handling requests. |
0baf5fac |
133 | |
134 | =cut |
135 | |
4ceac1a1 |
136 | sub pm_manage { |
518709ed |
137 | my ($this,%values) = self_or_default(@_); |
138 | map { $this->pm_parameter($_,$values{$_}) } keys %values; |
0baf5fac |
139 | |
518709ed |
140 | # skip to handling now if we won't be managing any processes. |
c146b63d |
141 | $this->n_processes() or goto HANDLING; |
0baf5fac |
142 | |
518709ed |
143 | # call the (possibly overloaded) management initialization hook. |
144 | $this->role("manager"); |
c146b63d |
145 | $this->managing_init(); |
518709ed |
146 | $this->pm_notify("initialized"); |
0baf5fac |
147 | |
518709ed |
148 | my $manager_pid = $$; |
0baf5fac |
149 | |
518709ed |
150 | MANAGING_LOOP: while (1) { |
151 | |
152 | # if the calling process goes away, perform cleanup. |
153 | getppid() == 1 and |
154 | return $this->pm_die("calling process has died"); |
0baf5fac |
155 | |
518709ed |
156 | $this->n_processes() > 0 or |
157 | return $this->pm_die(); |
0baf5fac |
158 | |
518709ed |
159 | # while we have fewer servers than we want. |
160 | PIDS: while (keys(%{$this->{PIDS}}) < $this->n_processes()) { |
161 | |
162 | if (my $pid = fork()) { |
163 | # the manager remembers the server. |
0baf5fac |
164 | $this->{PIDS}->{$pid} = { pid=>$pid }; |
518709ed |
165 | $this->pm_notify("server (pid $pid) started"); |
0baf5fac |
166 | |
167 | } elsif (! defined $pid) { |
518709ed |
168 | return $this->pm_abort("fork: $!"); |
0baf5fac |
169 | |
170 | } else { |
518709ed |
171 | $this->role("server"); |
172 | $this->{MANAGER_PID} = $manager_pid; |
173 | # the server exits the managing loop. |
174 | last MANAGING_LOOP; |
0baf5fac |
175 | } |
0baf5fac |
176 | |
518709ed |
177 | for (my $s = $this->start_delay(); $s; $s = sleep $s) {}; |
178 | } |
c146b63d |
179 | |
518709ed |
180 | # this should block until the next server dies. |
c2bbadb3 |
181 | $this->pm_wait(); |
0baf5fac |
182 | |
183 | }# while 1 |
184 | |
518709ed |
185 | HANDLING: |
0baf5fac |
186 | |
c2bbadb3 |
187 | # forget any children we had been collecting. |
188 | delete $this->{PIDS}; |
189 | |
518709ed |
190 | # call the (possibly overloaded) handling init hook |
191 | $this->role("server"); |
c146b63d |
192 | $this->handling_init(); |
518709ed |
193 | $this->pm_notify("initialized"); |
0baf5fac |
194 | |
518709ed |
195 | # server returns |
0baf5fac |
196 | return 1; |
197 | } |
198 | |
c146b63d |
199 | =head2 managing_init |
0baf5fac |
200 | |
c2bbadb3 |
201 | instance |
202 | () managing_init() |
203 | |
204 | DESCRIPTION: |
205 | |
206 | Overrideable method which initializes a process manager. In order to |
207 | handle signals, manage the PID file, and change the process name properly, |
208 | any method which overrides this should call SUPER::managing_init(). |
209 | |
0baf5fac |
210 | =cut |
211 | |
c146b63d |
212 | sub managing_init { |
c2bbadb3 |
213 | my ($this) = @_; |
0baf5fac |
214 | |
518709ed |
215 | # begin to handle signals. |
216 | $SIG{TERM} = sub { $this->sig_manager(@_) }; |
217 | $SIG{HUP} = sub { $this->sig_manager(@_) }; |
0baf5fac |
218 | |
518709ed |
219 | # change the name of this process as it appears in ps(1) output. |
220 | $this->pm_change_process_name("perl-fcgi-pm"); |
0baf5fac |
221 | |
518709ed |
222 | $this->pm_write_pid_file(); |
0baf5fac |
223 | } |
224 | |
518709ed |
225 | |
226 | =head2 pm_die |
0baf5fac |
227 | |
c2bbadb3 |
228 | instance or export |
229 | () pm_die(string msg[, int exit_status]) |
230 | |
231 | DESCRIPTION: |
232 | |
233 | This method is called when a process manager receives a notification to |
234 | shut itself down. pm_die() attempts to shutdown the process manager |
235 | gently, sending a SIGTERM to each managed process, waiting die_timeout() |
236 | seconds to reap each process, and then exit gracefully once all children |
237 | are reaped, or to abort if all children are not reaped. |
238 | |
0baf5fac |
239 | =cut |
240 | |
518709ed |
241 | sub pm_die { |
242 | my ($this,$msg,$n) = self_or_default(@_); |
243 | |
244 | # stop handling signals. |
245 | $SIG{HUP} = 'DEFAULT'; |
246 | $SIG{TERM} = 'DEFAULT'; |
247 | |
248 | $this->pm_remove_pid_file(); |
249 | |
250 | # prepare to die no matter what. |
251 | if (defined $this->die_timeout()) { |
c2bbadb3 |
252 | $SIG{ARLM} = sub { $this->pm_abort("wait timeout") }; |
518709ed |
253 | alarm $this->die_timeout(); |
254 | } |
255 | |
256 | # send a TERM to each of the servers. |
257 | kill "TERM", keys %{$this->{PIDS}}; |
258 | |
259 | # wait for the servers to die. |
260 | while (%{$this->{PIDS}}) { |
c2bbadb3 |
261 | $this->pm_wait(); |
518709ed |
262 | } |
263 | |
264 | # die already. |
265 | $this->pm_exit("dying: ".$msg,$n); |
0baf5fac |
266 | } |
267 | |
c2bbadb3 |
268 | =head2 pm_wait |
269 | |
270 | instance or export |
271 | (int pid) pm_wait() |
272 | |
273 | DESCRIPTION: |
274 | |
275 | This calls wait() which suspends execution until a child has exited. |
276 | If the process ID returned by wait corresponds to a managed process, |
277 | pm_notify() is called with the exit status of that process. |
278 | pm_wait() returns with the return value of wait(). |
0baf5fac |
279 | |
280 | =cut |
281 | |
c2bbadb3 |
282 | sub pm_wait { |
4ceac1a1 |
283 | my ($this) = self_or_default(@_); |
518709ed |
284 | |
285 | # wait for the next server to die. |
286 | next if (my $pid = wait()) < 0; |
287 | |
288 | # notify when one of our servers have died. |
289 | delete $this->{PIDS}->{$pid} and |
290 | $this->pm_notify("server (pid $pid) exited with status $?"); |
c2bbadb3 |
291 | |
292 | return $pid; |
0baf5fac |
293 | } |
294 | |
c146b63d |
295 | =head2 pm_write_pid_file |
0baf5fac |
296 | |
c2bbadb3 |
297 | instance or export |
298 | () pm_write_pid_file([string filename]) |
299 | |
300 | DESCRIPTION: |
301 | |
302 | Writes current process ID to optionally specified file. If no filename is |
303 | specified, it uses the value of the C<pid_fname> parameter. |
304 | |
0baf5fac |
305 | =cut |
306 | |
4ceac1a1 |
307 | sub pm_write_pid_file { |
308 | my ($this,$fname) = self_or_default(@_); |
0baf5fac |
309 | $fname ||= $this->pid_fname() or return; |
310 | if (!open PIDFILE, ">$fname") { |
518709ed |
311 | $this->pm_warn("open: $fname: $!"); |
0baf5fac |
312 | return; |
313 | } |
314 | print PIDFILE "$$\n"; |
315 | close PIDFILE; |
316 | } |
317 | |
c146b63d |
318 | =head2 pm_remove_pid_file |
0baf5fac |
319 | |
c2bbadb3 |
320 | instance or export |
321 | () pm_remove_pid_file() |
322 | |
323 | DESCRIPTION: |
324 | |
325 | Removes optionally specified file. If no filename is specified, it uses |
326 | the value of the C<pid_fname> parameter. |
327 | |
0baf5fac |
328 | =cut |
329 | |
4ceac1a1 |
330 | sub pm_remove_pid_file { |
331 | my ($this,$fname) = self_or_default(@_); |
0baf5fac |
332 | $fname ||= $this->pid_fname() or return; |
518709ed |
333 | my $ret = unlink($fname) or $this->pm_warn("unlink: $fname: $!"); |
0baf5fac |
334 | return $ret; |
335 | } |
336 | |
518709ed |
337 | =head2 sig_manager |
0baf5fac |
338 | |
c2bbadb3 |
339 | instance |
340 | () sig_manager(string name) |
341 | |
342 | DESCRIPTION: |
343 | |
344 | Handles signals of the process manager. Takes as input the name of signal |
345 | being handled. |
346 | |
0baf5fac |
347 | =cut |
348 | |
518709ed |
349 | sub sig_manager { |
350 | my ($this,$name) = @_; |
351 | if ($name eq "TERM" or $name eq "HUP") { |
c2bbadb3 |
352 | $this->pm_notify("received signal $name"); |
353 | $this->pm_die("safe exit from signal $name"); |
518709ed |
354 | } else { |
355 | $this->pm_notify("ignoring signal $name"); |
356 | } |
0baf5fac |
357 | } |
358 | |
518709ed |
359 | =head1 Handler methods |
0baf5fac |
360 | |
518709ed |
361 | =head2 handling_init |
0baf5fac |
362 | |
c2bbadb3 |
363 | instance or export |
364 | () handling_init() |
365 | |
366 | DESCRIPTION: |
367 | |
0baf5fac |
368 | =cut |
369 | |
518709ed |
370 | sub handling_init { |
c2bbadb3 |
371 | my ($this) = @_; |
0baf5fac |
372 | |
518709ed |
373 | # begin to handle signals. |
374 | $SIG{TERM} = sub { $this->sig_handler(@_) }; |
375 | $SIG{HUP} = sub { $this->sig_handler(@_) }; |
0baf5fac |
376 | |
518709ed |
377 | # change the name of this process as it appears in ps(1) output. |
378 | $this->pm_change_process_name("perl-fcgi"); |
0baf5fac |
379 | } |
380 | |
518709ed |
381 | =head2 pm_pre_dispatch |
0baf5fac |
382 | |
c2bbadb3 |
383 | instance or export |
384 | () pm_pre_dispatch() |
385 | |
386 | DESCRIPTION: |
387 | |
0baf5fac |
388 | =cut |
389 | |
518709ed |
390 | sub pm_pre_dispatch { |
4ceac1a1 |
391 | my ($this) = self_or_default(@_); |
0baf5fac |
392 | } |
393 | |
518709ed |
394 | =head2 pm_post_dispatch |
0baf5fac |
395 | |
c2bbadb3 |
396 | instance or export |
397 | () pm_post_dispatch() |
398 | |
399 | DESCRIPTION: |
400 | |
0baf5fac |
401 | =cut |
402 | |
518709ed |
403 | sub pm_post_dispatch { |
4ceac1a1 |
404 | my ($this) = self_or_default(@_); |
518709ed |
405 | if ($this->pm_received_signal("TERM")) { |
406 | $this->pm_exit("safe exit after SIGTERM"); |
407 | } |
408 | if ($this->pm_received_signal("HUP")) { |
409 | $this->pm_exit("safe exit after SIGHUP"); |
410 | } |
c2bbadb3 |
411 | if ($this->{MANAGER_PID} and getppid() != $this->{MANAGER_PID}) { |
518709ed |
412 | $this->pm_exit("safe exit: manager has died"); |
413 | } |
0baf5fac |
414 | } |
415 | |
518709ed |
416 | =head2 sig_handler |
0baf5fac |
417 | |
c2bbadb3 |
418 | instance or export |
419 | () sig_handler() |
420 | |
421 | DESCRIPTION: |
422 | |
0baf5fac |
423 | =cut |
424 | |
518709ed |
425 | sub sig_handler { |
c146b63d |
426 | my ($this,$name) = @_; |
518709ed |
427 | $this->pm_received_signal($name,1); |
428 | } |
429 | |
430 | =head1 Common methods and routines |
431 | |
432 | =head2 self_or_default |
433 | |
434 | private global |
435 | (ProcManager, @args) self_or_default([ ProcManager, ] @args); |
436 | |
437 | DESCRIPTION: |
438 | |
439 | This is a helper subroutine to acquire or otherwise create a singleton |
440 | default object if one is not passed in, e.g., a method call. |
441 | |
442 | =cut |
443 | |
444 | sub self_or_default { |
445 | return @_ if defined $_[0] and !ref $_[0] and $_[0] eq 'FCGI::ProcManager'; |
446 | if (!defined $_[0] or (ref($_[0]) ne 'FCGI::ProcManager' and |
447 | !UNIVERSAL::isa($_[0],'FCGI::ProcManager'))) { |
448 | $Q or $Q = $FCGI::ProcManager::Default->new; |
449 | unshift @_, $Q; |
0baf5fac |
450 | } |
518709ed |
451 | return wantarray ? @_ : $Q; |
452 | } |
453 | |
454 | =head2 pm_change_process_name |
455 | |
c2bbadb3 |
456 | instance or export |
457 | () pm_change_process_name() |
458 | |
459 | DESCRIPTION: |
460 | |
518709ed |
461 | =cut |
462 | |
463 | sub pm_change_process_name { |
464 | my ($this,$name) = self_or_default(@_); |
465 | $0 = $name; |
466 | } |
467 | |
468 | =head2 pm_received_signal |
469 | |
c2bbadb3 |
470 | instance or export |
471 | () pm_received signal() |
472 | |
473 | DESCRIPTION: |
474 | |
518709ed |
475 | =cut |
476 | |
477 | sub pm_received_signal { |
478 | my ($this,$sig,$received) = self_or_default(@_); |
479 | $sig or return $this->{SIG_RECEIVED}; |
480 | $received and $this->{SIG_RECEIVED}->{$sig}++; |
481 | return $this->{SIG_RECEIVED}->{$sig}; |
482 | } |
483 | |
c2bbadb3 |
484 | =head1 parameters |
485 | |
518709ed |
486 | =head2 pm_parameter |
487 | |
c2bbadb3 |
488 | instance or export |
489 | () pm_parameter() |
490 | |
491 | DESCRIPTION: |
492 | |
518709ed |
493 | =cut |
494 | |
495 | sub pm_parameter { |
496 | my ($this,$key,$value) = self_or_default(@_); |
497 | defined $value and $this->{$key} = $value; |
498 | return $this->{$key}; |
0baf5fac |
499 | } |
500 | |
518709ed |
501 | =head2 n_processes |
502 | |
503 | =head2 no_signals |
504 | |
505 | =head2 pid_fname |
506 | |
507 | =head2 die_timeout |
508 | |
509 | =head2 role |
510 | |
511 | =head2 start_delay |
512 | |
c2bbadb3 |
513 | DESCRIPTION: |
514 | |
518709ed |
515 | =cut |
516 | |
517 | sub n_processes { shift->pm_parameter("n_processes", @_); } |
518 | sub pid_fname { shift->pm_parameter("pid_fname", @_); } |
519 | sub no_signals { shift->pm_parameter("no_signals", @_); } |
520 | sub die_timeout { shift->pm_parameter("die_timeout", @_); } |
521 | sub role { shift->pm_parameter("role", @_); } |
522 | sub start_delay { shift->pm_parameter("start_delay", @_); } |
523 | |
c2bbadb3 |
524 | =head1 notification and death |
525 | |
c146b63d |
526 | =head2 pm_warn |
0baf5fac |
527 | |
c2bbadb3 |
528 | instance or export |
529 | () pm_warn() |
530 | |
531 | DESCRIPTION: |
532 | |
0baf5fac |
533 | =cut |
534 | |
4ceac1a1 |
535 | sub pm_warn { |
536 | my ($this,$msg) = self_or_default(@_); |
518709ed |
537 | $this->pm_notify($msg); |
538 | } |
539 | |
540 | =head2 pm_notify |
541 | |
c2bbadb3 |
542 | instance or export |
543 | () pm_notify() |
544 | |
545 | DESCRIPTION: |
546 | |
518709ed |
547 | =cut |
548 | |
549 | sub pm_notify { |
550 | my ($this,$msg) = self_or_default(@_); |
551 | $msg =~ s/\s*$/\n/; |
552 | print STDERR "FastCGI: ".$this->role()." (pid $$): ".$msg; |
0baf5fac |
553 | } |
554 | |
c2bbadb3 |
555 | =head2 pm_exit |
556 | |
557 | instance or export |
558 | () pm_exit(string msg[, int exit_status]) |
559 | |
560 | DESCRIPTION: |
0baf5fac |
561 | |
562 | =cut |
563 | |
4ceac1a1 |
564 | sub pm_exit { |
565 | my ($this,$msg,$n) = self_or_default(@_); |
0baf5fac |
566 | $n ||= 0; |
c2bbadb3 |
567 | |
568 | # if we still have children at this point, something went wrong. |
569 | # SIGKILL them now. |
570 | kill "KILL", keys %{$this->{PIDS}} if $this->{PIDS}; |
571 | |
4ceac1a1 |
572 | $this->pm_warn($msg); |
0baf5fac |
573 | $@ = $msg; |
574 | exit $n; |
575 | } |
576 | |
c146b63d |
577 | =head2 pm_abort |
0baf5fac |
578 | |
c2bbadb3 |
579 | instance or export |
580 | () pm_abort(string msg[, int exit_status]) |
581 | |
582 | DESCRIPTION: |
583 | |
0baf5fac |
584 | =cut |
585 | |
4ceac1a1 |
586 | sub pm_abort { |
587 | my ($this,$msg,$n) = self_or_default(@_); |
0baf5fac |
588 | $n ||= 1; |
4ceac1a1 |
589 | $this->pm_exit($msg,1); |
0baf5fac |
590 | } |
591 | |
592 | 1; |
593 | __END__ |
594 | |
595 | =head1 BUGS |
596 | |
597 | No known bugs, but this does not mean no bugs exist. |
598 | |
599 | =head1 SEE ALSO |
600 | |
601 | L<FCGI>. |
602 | |
603 | =head1 COPYRIGHT |
604 | |
605 | FCGI-ProcManager - A Perl FCGI Process Manager |
606 | Copyright (c) 2000, FundsXpress Financial Network, Inc. |
607 | |
608 | This library is free software; you can redistribute it and/or |
609 | modify it under the terms of the GNU Lesser General Public |
610 | License as published by the Free Software Foundation; either |
611 | version 2 of the License, or (at your option) any later version. |
612 | |
613 | BECAUSE THIS LIBRARY IS LICENSED FREE OF CHARGE, THIS LIBRARY IS |
614 | BEING PROVIDED "AS IS WITH ALL FAULTS," WITHOUT ANY WARRANTIES |
615 | OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT |
616 | LIMITATION, ANY IMPLIED WARRANTIES OF TITLE, NONINFRINGEMENT, |
617 | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, AND THE |
618 | ENTIRE RISK AS TO SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, |
619 | AND EFFORT IS WITH THE YOU. See the GNU Lesser General Public |
620 | License for more details. |
621 | |
622 | You should have received a copy of the GNU Lesser General Public |
623 | License along with this library; if not, write to the Free Software |
624 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
625 | |
626 | =cut |