File Coverage

blib/lib/WebDyne/Util.pm
Criterion Covered Total %
statement 41 151 27.1
branch 8 78 10.2
condition 3 59 5.0
subroutine 13 19 68.4
pod 0 8 0.0
total 65 315 20.6


line stmt bran cond sub pod time code
1             #
2             # This file is part of WebDyne.
3             #
4             # This software is copyright (c) 2026 by Andrew Speer .
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9             # Full license text is available at:
10             #
11             #
12             #
13              
14             package WebDyne::Util;
15              
16              
17             # Compiler Pragma
18             #
19 9     9   316882 sub BEGIN {$^W=0}
20 9     9   58 use strict qw(vars);
  9         16  
  9         379  
21 9     9   64 use vars qw($VERSION @EXPORT);
  9         43  
  9         500  
22 9     9   45 use warnings;
  9         14  
  9         544  
23 9     9   59 no warnings qw(uninitialized redefine once);
  9         15  
  9         458  
24              
25              
26             # External modules
27             #
28 9     9   2533 use Data::Dumper;
  9         40358  
  9         553  
29 9     9   3577 use IO::File;
  9         64589  
  9         1468  
30 9     9   5065 use POSIX qw(strftime);
  9         73719  
  9         58  
31              
32              
33             # Use Exporter
34             #
35             require Exporter;
36              
37              
38             # Exports
39             #
40             @EXPORT=qw(err errstr errclr errdump errsubst errstack errnofatal debug);
41              
42              
43             # Version information
44             #
45             $VERSION='2.075';
46              
47              
48             # Var to hold package wide hash, for data shared across package, and error stack
49             #
50             my (%Package, @Err);
51              
52              
53             # Bring the WEBDYNE_DEBUG env var into package var so available via Plack handler,
54             # which normally replaces env with its own
55             #
56             $Package{'WEBDYNE_DEBUG'}=$ENV{'WEBDYNE_DEBUG'};
57              
58              
59             # All done. Positive return
60             #
61             1;
62              
63              
64             #==================================================================================================
65              
66             # Packace init, attempt to load optional Time::HiRes module
67             #
68             BEGIN {
69 9     9   17412 eval {require Time::HiRes; Time::HiRes->import(qw(time gettimeofday))};
  9         1483  
  9         3345  
70             }
71              
72              
73             sub import {
74              
75              
76             # Get message
77             #
78 63     63   301 my ($message, @param)=@_;
79              
80              
81             # Get who is calling us
82             #
83 63   50     538 my $caller=(caller(0))[0] || return undef;
84              
85              
86             # fn, fh we will write to
87             #
88 63         175 my ($debug_fn, $debug_fh);
89              
90              
91             # Environment var overrides all
92             #
93 63 50 33     352 if ($debug_fn=$ENV{'WEBDYNE_DEBUG_FILE'}) {
    50          
    50          
    50          
94              
95             # fn is whatever spec'd
96             #
97 0   0     0 $debug_fh=IO::File->new($debug_fn, O_CREAT | O_APPEND | O_WRONLY) || do {
98             warn("unable to open file '$debug_fn', $!");
99             undef;
100             };
101              
102             }
103             elsif ($ENV{'WEBDYNE_DEBUG'}) {
104              
105              
106             # fh is stderr
107             #
108 0         0 $debug_fh=\*STDERR;
109              
110              
111             }
112 63         555 elsif (ref(my $debug_hr=${"${caller}::DEBUG"}) eq 'HASH') {
113              
114              
115             # Debug is hash ref, extract filename etc and open
116             #
117 0   0     0 $debug_fn=$debug_hr->{'file'} || $debug_hr->{'filename'};
118 0         0 my ($mode, $package)=@{$debug_hr}{qw(mode package)};
  0         0  
119 0 0 0     0 if ($debug_fn && ($package ? ($package eq $caller) : 1)) {
    0          
    0          
120 0   0     0 $mode ||= O_CREAT | O_APPEND | O_WRONLY;
121             $debug_fh=(
122             $Package{'debug_fh'}{$debug_fn} ||= (
123 0   0     0 IO::File->new($debug_fn, $mode) || do {
      0        
124             warn("unable to open file '$debug_fn', $!");
125             undef;
126             }
127             ));
128             }
129             elsif (!$debug_fn) {
130 0         0 warn(sprintf('no file name specified in DEBUG hash %s', Dumper($debug_hr)));
131             }
132              
133             }
134 63         307 elsif (!ref($debug_fn=${"${caller}::DEBUG"}) && ${"${caller}::DEBUG"}) {
  63         289  
135              
136             # Just file name spec'd. Open
137             #
138             $debug_fh=(
139             $Package{'debug_fh'}{$debug_fn} ||= (
140 0   0     0 IO::File->new($debug_fn, O_CREAT | O_APPEND | O_WRONLY) || do {
      0        
141             warn("unable to open file '$debug_fn', $!");
142             undef;
143             }
144             ));
145             }
146              
147              
148             # After all that did we get a file handle ? If so, import the debug handler
149             #
150 63 50       154 if ($debug_fh) {
151              
152             # Yes, setup debug routine
153             #
154 0         0 $debug_fh->autoflush(1);
155 0         0 $Package{'debug_fh'}=$debug_fh;
156              
157 0         0 if (0) { # Don't do it this way anymore, use a proper debug function and export
158             *{"${caller}::debug"}=sub {
159 0     0   0 local $|=1;
160 0   0     0 my $method=(caller(1))[3] || 'main';
161 0         0 (my $subroutine=$method)=~s/^.*:://;
162 0 0 0     0 if ($ENV{'WEBDYNE_DEBUG'} && ($ENV{'WEBDYNE_DEBUG'} ne '1')) {
163 0         0 my @debug_target=split(/[,;]/, $ENV{'WEBDYNE_DEBUG'});
164 0         0 foreach my $debug_target (@debug_target) {
165 0 0 0     0 if (($caller eq $debug_target) || ($method=~/\Q$debug_target\E$/)) {
166 0         0 CORE::print $debug_fh "[$subroutine] ", sprintf(shift(), @_), $/;
167             }
168             }
169             }
170             else {
171 0 0       0 CORE::print $debug_fh "[$subroutine] ", $_[1] ? sprintf(shift(), @_) : $_[0], $/;
172             }
173             }
174             unless UNIVERSAL::can($caller, 'debug');
175             *{"${caller}::Dumper"}=\&Data::Dumper::Dumper
176             unless UNIVERSAL::can($caller, 'Dumper');
177             }
178              
179             }
180             else {
181              
182             # No, null our debug and Dumper routine
183             #
184             #*{"${caller}::debug"}=sub { }
185             # unless UNIVERSAL::can($caller, 'debug');
186             #*{"${caller}::Dumper"}=sub { }
187             # unless UNIVERSAL::can($caller, 'Dumper');
188              
189             }
190              
191              
192             # Setup file handle for error backtrace
193             #
194 63 50       97 if (my $fn=${"${caller}::ERROR"}) {
  63         311  
195              
196             # Just file name spec'd. Log
197             #
198 0         0 $Package{'error_fn'}{$fn}++
199              
200             }
201              
202              
203             # Done
204             #
205 63         376253 goto &Exporter::import;
206              
207             }
208              
209              
210             sub debug {
211              
212              
213             # Send debug message to log file. Turn off buffering and get file handle
214             #
215 30     30 0 11728 local $|=1;
216 30   50     218 my $debug_fh=$Package{'debug_fh'} ||
217             return undef;
218              
219              
220             # Get caller
221             #
222             # Get who is calling us
223             #
224 0   0     0 my $caller=(caller(0))[0] ||
225             return undef;
226 0   0     0 my $method=(caller(1))[3] || 'main';
227 0         0 (my $subroutine=$method)=~s/^.*:://;
228 0         0 (my $class=$method)=~s/::\Q${subroutine}\E$//;
229              
230              
231             # Time in human readable format
232             #
233 0         0 my ($sec, $msec)=gettimeofday();
234 0         0 my $timestamp=strftime("%H:%M:%S", localtime($sec)) . sprintf('.%06d', $msec);
235              
236              
237             # Get the debug message
238             #
239             #local $SIG{__WARN__}=sub { require Carp; &Carp::confess @_ }; #uncomment if want to trace any missing sprintf params
240 0 0       0 my $debug=$#_ ? sprintf(shift(), @_) : shift();
241              
242              
243             # Filtering ?
244             #
245 0 0 0     0 if ($Package{'WEBDYNE_DEBUG'} && ($Package{'WEBDYNE_DEBUG'} ne '1')) {
246              
247              
248             # Yes - check we are getting from caller we are interested in
249             #
250 0         0 my @debug_target=split(/[,;]/, $Package{'WEBDYNE_DEBUG'});
251 0         0 foreach my $debug_target (@debug_target) {
252 0 0 0     0 if (($caller eq $debug_target) || ($method=~/\Q$debug_target\E$/)) {
253            
254             # Print debug after checking for any regexp wanted
255             #
256 0 0       0 if (my $regexp=$ENV{'WEBDYNE_DEBUG_FILTER'}) {
257 0 0       0 next unless $debug=~qr/$regexp/m;
258             }
259 0         0 CORE::print $debug_fh "[$timestamp $class ($subroutine)] ", $debug, $/;
260             }
261             }
262             }
263             else {
264              
265             # No filtering. Open floodgates but still apply any regexp
266             #
267 0 0       0 if (my $regexp=$ENV{'WEBDYNE_DEBUG_FILTER'}) {
268 0 0       0 return unless $debug=~qr/$regexp/;
269             }
270 0         0 CORE::print $debug_fh "[$timestamp $class ($subroutine)] ", $debug, $/;
271             }
272              
273             }
274              
275              
276             sub errnofatal {
277              
278              
279             #
280             #
281 7 50   7 0 38 @_ ? $Package{'nofatal'}=@_ : $Package{'nofatal'};
282              
283              
284             }
285              
286              
287             sub err {
288              
289              
290             # Get the message and any sprintf params
291             #
292 0     0 0 0 my ($message, @param)=@_;
293              
294              
295             # If no message supplied return last one seen
296             #
297 0 0       0 unless ($message) {
298 0 0 0     0 $message=@Err ? $Err[$#Err]->[0] && return undef : 'undefined error';
299             }
300             else {
301 0 0       0 $message=sprintf($message, @param) if @param;
302             }
303              
304              
305             # Init the caller var and array
306             #
307 0         0 my @caller;
308 0         0 my $caller=(caller(0))[0];
309              
310              
311             # Populate the caller array
312             #
313 0         0 for (my $i=0; my @info=(caller($i))[0..3]; $i++) {
314              
315              
316             # Push onto the caller array
317             #
318 0         0 push @caller, \@info;
319              
320              
321             }
322              
323              
324             # If this message is *not* the same as the last one we saw,
325             # we will log it
326             #
327 0 0 0     0 unless ($message eq (@Err && $Err[0]->[0])) {
328              
329              
330             # Add to stack
331             #
332 0         0 unshift @Err, [$message, @caller];
333              
334              
335             # If caller has a debug function enabled, call this with the warning
336             #
337 0 0       0 if (UNIVERSAL::can($caller, 'debug')) {
338              
339              
340             # Yes, they are using the debug module, so can we call it
341             #
342 0         0 &{"${caller}::debug"}($message);
  0         0  
343              
344              
345             }
346              
347              
348             # Dump to backtrace file if enabled
349             #
350 0         0 foreach my $fn (keys %{$Package{'error_fn'}}) {
  0         0  
351              
352 0 0       0 unless (my $fh=IO::File->new($fn, O_CREAT | O_APPEND | O_WRONLY)) {
353 0         0 warn("unable to open file '$fn', $!");
354             }
355             else {
356 0         0 seek($fh, 0, 2); # Seek to EOF
357 0         0 my $errdump=&errdump();
358 0         0 CORE::print $fh $errdump, $/, $/;
359 0         0 $fh->close();
360             }
361              
362             }
363              
364              
365             }
366              
367              
368             # Return undef
369             #
370 0 0       0 return $Package{'nofatal'} ? undef : die(&errdump);
371              
372             }
373              
374              
375             sub errstr {
376              
377              
378             # Check that there are messages in the stack before trying to get
379             # the last one
380             #
381 62 50   62 0 243 if (my $count=@Err) {
382              
383              
384             # There are objects in the array, so it is safe to do a fetch
385             # on the last (-1) array slot
386             #
387 0         0 my $errstr=$Err[--$count]->[0];
388              
389              
390             # And return the errstr
391             #
392 0         0 return $errstr;
393              
394             }
395             else {
396              
397              
398             # Nothing in the array stack, return undef
399             #
400 62         287 return undef;
401              
402              
403             }
404              
405             }
406              
407              
408             sub errclr {
409              
410              
411             # Clear the warning stack
412             #
413 0     0 0   undef @Err;
414              
415              
416             # Replace errors if args
417             #
418 0 0         @_ && (return &err(@_));
419              
420              
421             # Return OK always
422             #
423 0           return 1;
424              
425             }
426              
427              
428             sub errsubst {
429              
430              
431             # Replace the current error message with a new one, keeping callback
432             # stack
433             #
434 0     0 0   my ($message, @param)=@_;
435              
436             # If no message supplied return last one seen
437             #
438 0 0         unless ($message) {
439 0 0 0       $message=@Err ? $Err[$#Err]->[0] && return undef : 'undefined error';
440             }
441             else {
442 0           $message=sprintf($message, @param);
443             }
444              
445             # Chomp the message
446             #
447 0           chomp($message);
448              
449              
450             # Replace if present, define if not
451             #
452 0 0         @Err ? ($Err[$#Err]->[0]=$message) : goto &err;
453              
454              
455             # Return
456             #
457 0           return undef;
458              
459              
460             }
461              
462              
463             sub errdump {
464              
465              
466             # Use can send additional info to dump as key/value pairs in hash ref
467             # supplied as arg
468             #
469 0     0 0   my $info_hr=shift();
470              
471              
472             # Return a dump of error in a nice format, no params. Do this with
473             # format strings, so define the ones we will use
474             #
475 0           my @format=(
476              
477             '+' . ('-' x 78) . "+\n",
478             "| @<<<<< | ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< |\n",
479             "| | ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~ |\n"
480              
481             );
482              
483              
484             # Go through the message stack on error at a time in reverse order
485             #
486 0           foreach my $err_ar (reverse @Err) {
487              
488              
489             # Get message, clean up
490             #
491 0           my $message=ucfirst($err_ar->[0]);
492 0           $message=~s/\s+$//;
493 0 0         $message.='.' unless $message=~/[\.\!\?]$/;
494 0           my @message=split("\n", $message);
495 0 0         $message=shift @message if @message;
496              
497              
498             # Print out date, time, error message
499             #
500 0           formline $format[0];
501 0           formline $format[1], 'Date', scalar(localtime());
502 0           formline $format[0];
503 0           formline $format[1], 'Error', $message;
504 0 0         (formline $format[2], $message) if $message;
505 0 0         map {formline $format[2], $_} @message if @message;
  0            
506 0           formline $format[0];
507              
508              
509             # Flag so we know we have printed the caller field
510             #
511 0           my $caller_fg;
512              
513              
514             # Go through callback stack
515             #
516 0           for (my $i=1; defined($err_ar->[$i]); $i++) {
517              
518              
519             # Get method, line no and file
520             #
521 0   0       my $method=$err_ar->[$i+1][3] || $err_ar->[$i][0] || last;
522 0   0       my $lineno=$err_ar->[$i][2] || next;
523 0           my $filenm=$err_ar->[$i][1];
524              
525              
526             # Print them out, print out caller label unless we
527             # have already done so
528             #
529 0 0         formline $format[1],
530             $caller_fg++ ? '' : 'Caller', "$method, line $lineno";
531              
532             }
533              
534              
535             # Include any user supplied info
536             #
537 0           while (my ($key, $value)=each %{$info_hr}) {
  0            
538              
539              
540             # Print separator, info
541             #
542 0           formline $format[0];
543 0           formline $format[1], $key, $value;
544 0 0         (formline $format[2], $value) if $value;
545              
546             }
547              
548              
549             # Finish off formatting, print PID. Dont ask me why $$ has to be "$$",
550             # it does not show up any other way
551             #
552 0           formline $format[0];
553 0           formline $format[1], 'PID', "$$";
554 0           formline $format[0];
555 0           formline "\n";
556              
557              
558             }
559              
560              
561             # Empty the format accumulator and return it
562             #
563 0           my $return=$^A; undef $^A;
  0            
564 0           return $return;
565              
566             }
567              
568              
569             sub errstack {
570              
571             # Return or push the raw error stack
572             #
573 0 0   0 0   return @_ ? \(@Err=@{$_[1]}) : \@Err;
  0            
574              
575             }
576