File Coverage

File:blib/lib/Object/Configure.pm
Coverage:80.5%

linestmtbrancondsubtimecode
1package Object::Configure;
2
3
11
11
11
798825
9
148
use strict;
4
11
11
11
17
7
188
use warnings;
5
6
11
11
11
17
10
276
use Carp;
7
11
11
11
2254
312914
155
use Config::Abstraction 0.37;
8
11
11
11
28
9
148
use File::Spec;
9
11
11
11
2364
129034
207
use Log::Abstraction 0.26;
10
11
11
11
26
51
155
use Params::Get 0.13;
11
11
11
11
20
9
132
use Return::Set;
12
11
11
11
13
5
184
use Scalar::Util qw(blessed weaken);
13
11
11
11
17
8
40
use Time::HiRes qw(time);
14
11
11
11
2655
29145
7600
use File::stat;
15
16# Global registry to track configured objects for hot reload
17our %_object_registry = ();
18our %_config_watchers = ();
19our %_config_file_stats = ();
20
21# Keep track of the original USR1 handler for chaining
22our $_original_usr1_handler;
23
24 - 32
=head1 NAME

Object::Configure - Runtime Configuration for an Object

=head1 VERSION

0.19

=cut
33
34our $VERSION = 0.19;
35
36 - 309
=head1 SYNOPSIS

The C<Object::Configure> module is a lightweight utility designed to inject runtime parameters into other classes,
primarily by layering configuration and logging support,
when instatiating objects.

L<Log::Abstraction> and L<Config::Abstraction> are modules developed to solve a specific need,
runtime configurability without needing to rewrite or hardcode behaviours.
The goal is to allow individual modules to enable or disable features on the fly,
and to do it using whatever configuration system the user prefers.

Although the initial aim was general configurability,
the primary use case that's emerged has been fine-grained logging control,
more flexible and easier to manage than what you'd typically do with L<Log::Log4perl>.
For example,
you might want one module to log verbosely while another stays quiet,
and be able to toggle that dynamically - without making invasive changes to each module.

To tie it all together,
there is C<Object::Configure>.
It sits on L<Log::Abstraction> and L<Config::Abstraction>,
and with just a couple of extra lines in a class constructor,
you can hook in this behaviour seamlessly.
The intent is to keep things modular and reusable,
especially across larger systems or in situations where you want user-selectable behaviour.

Add this to your constructor:

   package My::Module;

   use Object::Configure;
   use Params::Get;

   sub new {
        my $class = shift;
        my $params = Object::Configure::configure($class, @_ ? \@_ : undef);    # Reads in the runtime configuration settings
        # or my $params = Object::Configure::configure($class, { @_ });

        return bless $params, $class;
    }

Throughout your class, add code such as:

    sub method
    {
        my $self = shift;

        $self->{'logger'}->trace(ref($self), ': ', __LINE__, ' entering method');
    }

=head3 CONFIGURATION INHERITANCE

C<Object::Configure> supports configuration inheritance, allowing child classes to inherit and override configuration settings from their parent classes.
When a class is configured, the module automatically traverses the inheritance hierarchy (using C<@ISA>) and loads configuration files for each ancestor class in the chain.

Configuration files are loaded in order from the most general (base class) to the most specific (child class), with later files overriding earlier ones. For example, if C<My::Child::Class> inherits from C<My::Parent::Class>, which inherits from C<My::Base::Class>, the module will:

=over 4

=item 1. Load C<my-base-class.yml> (or .conf, .json, etc.) if it exists

=item 2. Load C<my-parent-class.yml> if it exists, overriding base settings

=item 3. Load C<my-child-class.yml>, overriding both parent and base settings

=back

The configuration files should be named using lowercase versions of the class name with C<::> replaced by hyphens (C<->).
For example, C<My::Parent::Class> would use C<my-parent-class.yml>.

This allows you to define common settings in a base class configuration file and selectively override them in child class configurations, promoting DRY (Don't Repeat Yourself) principles and making it easier to manage configuration across class hierarchies.

Example:

    # File: ~/.conf/my-base-class.yml
    ---
    My__Base__Class:
      timeout: 30
      retries: 3
      log_level: info

    # File: ~/.conf/my-child-class.yml
    ---
    My__Child__Class:
      timeout: 60
      # Inherits retries: 3 and log_level: info from parent

    # Result: Child class gets timeout=60, retries=3, log_level=info

Parent configuration files are optional.
If a parent class's configuration file doesn't exist, the module simply skips it and continues up the inheritance chain.
All discovered configuration files are tracked in the C<_config_files> array for hot reload support.

=head3 UNIVERSAL CONFIGURATION

All Perl classes implicitly inherit from C<UNIVERSAL>.
C<Object::Configure> takes advantage of this to provide a mechanism for universal configuration settings
that apply to all classes by default.

If you create a configuration file named C<universal.yml> (or C<universal.conf>, C<universal.json>, etc.)
in your configuration directory,
the settings in its C<UNIVERSAL> section will be inherited by all classes that use C<Object::Configure>,
unless explicitly overridden by class-specific configuration files.

This is particularly useful for setting application-wide defaults such as logging levels,
timeout values,
or other common parameters that should apply across all modules.

Example C<~/.conf/universal.yml>:

    ---
    UNIVERSAL:
      timeout: 30
      retries: 3
      logger:
        level: info

With this universal configuration file in place,
all classes will inherit these default values.
Individual classes can override any of these settings in their own configuration files:

Example C<~/.conf/my-special-class.yml>:

    ---
    My__Special__Class:
      timeout: 120
      # Inherits retries: 3 and logger.level: info from UNIVERSAL

The universal configuration is loaded first in the inheritance chain,
followed by parent class configurations,
and finally the specific class configuration,
with later configurations overriding earlier ones.

=head2 CHANGING BEHAVIOUR AT RUN TIME

=head3 USING A CONFIGURATION FILE

To control behavior at runtime, C<Object::Configure> supports loading settings from a configuration file via L<Config::Abstraction>.

A minimal example of a config file (C<~/.conf/local.conf>) might look like:

   [My__Module]
   logger.file = /var/log/mymodule.log

The C<configure()> function will read this file,
overlay it onto your default parameters,
and initialize the logger accordingly.

If the file is not readable and no config_dirs are provided,
the module will throw an error.
To be clear, in this case, inheritance is not followed.

This mechanism allows dynamic tuning of logging behavior (or other parameters you expose) without modifying code.

More details to be written.

=head3 USING ENVIRONMENT VARIABLES

C<Object::Configure> also supports runtime configuration via environment variables,
without requiring a configuration file.

Environment variables are read automatically when you use the C<configure()> function,
thanks to its integration with L<Config::Abstraction>.
These variables should be prefixed with your class name, followed by a double colon.

For example, to enable syslog logging for your C<My::Module> class,
you could set:

    export My__Module__logger__file=/var/log/mymodule.log

This would be equivalent to passing the following in your constructor:

     My::Module->new(logger => Log::Abstraction->new({ file => '/var/log/mymodule.log' });

All environment variables are read and merged into the default parameters under the section named after your class.
This allows centralized and temporary control of settings (e.g., for production diagnostics or ad hoc testing) without modifying code or files.

Note that environment variable settings take effect regardless of whether a configuration file is used,
and are applied during the call to C<configure()>.

More details to be written.

=head2 HOT RELOAD

Hot reload is not supported on Windows.

=head3 Basic Hot Reload Setup

    package My::App;
    use Object::Configure;

    sub new {
        my $class = shift;
        my $params = Object::Configure::configure($class, @_ ? \@_ : undef);
        my $self = bless $params, $class;

        # Register for hot reload
        Object::Configure::register_object($class, $self) if $params->{_config_file};

        return $self;
    }

    # Optional: Define a reload hook
    sub _on_config_reload {
        my ($self, $new_config) = @_;
        print "My::App config was reloaded!\n";
        # Custom reload logic here
    }

=head3 Enable Hot Reload in Your Main Application

    # Enable hot reload with custom callback
    Object::Configure::enable_hot_reload(
        interval => 5,  # Check every 5 seconds
        callback => sub {
            print "Configuration files have been reloaded!\n";
        }
    );

    # Your application continues running...
    # Config changes will be automatically detected and applied

=head3 Manual Reload

    # Manually trigger a reload
    my $count = Object::Configure::reload_config();
    print "Reloaded configuration for $count objects\n";

=head1 SUBROUTINES/METHODS

=head2 configure

Configure your class at runtime with hot reload support.

Takes arguments:

=over 4

=item * C<class>

=item * C<params>

A hashref containing default parameters to be used in the constructor.

=item * C<carp_on_warn>

If set to 1, call C<Carp::carp> on C<warn()>.
This value is also read from the configuration file,
which will take precedence.
The default is 0.

=item * C<croak_on_error>

If set to 1, call C<Carp::croak> on C<error()>.
This value is also read from the configuration file,
which will take precedence.
The default is 1.

=item * C<logger>

The logger to use.
If none is given, an instatiation of L<Log::Abstraction> will be created, unless the logger is set to NULL.

=item * C<schema>

A L<Params::Validate::Strict> compatible schema to validate the configuration file against.

=back

Returns a hash ref containing the new values for the constructor.

Now you can set up a configuration file and environment variables to configure your object.

=cut
310
311sub configure {
312
42
2217414
        my $class = $_[0];
313
42
93
        my $params = $_[1] || {};       # Contains the defaults, the run time config will overwrite them
314
42
46
        my $array;
315
316
42
53
        croak(__PACKAGE__, ': configure: what class do you want to configure?') if(!defined($class));
317
318
42
77
        if(exists($params->{'logger'}) && (ref($params->{'logger'}) eq 'ARRAY')) {
319
0
0
                $array = delete $params->{'logger'};
320        }
321
322
42
36
        my $original_class = $class;
323
42
91
        $class =~ s/::/__/g;
324
325        # Store config file path for hot reload
326
42
64
        my $config_file = $params->{'config_file'};
327
42
47
        my $config_dirs = $params->{'config_dirs'};
328
329        # Get inheritance chain for finding ancestor config files
330
42
63
        my @inheritance_chain = _get_inheritance_chain($original_class);
331
332        # Build list of config files to load (ancestor to child order)
333
42
36
        my @config_files_to_load = ();
334
42
43
        my %tracked_files = ();
335
336
42
42
        if ($config_file) {
337                # Check if primary config file is readable (unless config_dirs provided)
338
32
164
                if ((!$config_dirs) && (!-r $config_file)) {
339
2
43
                        croak("$class: ", $config_file, ": $!");
340                }
341
342                # Find config files for each class in the hierarchy
343                # Important: iterate in reverse order (base -> parent -> child)
344
30
34
                foreach my $ancestor_class (reverse @inheritance_chain) {
345
74
1139
                        my $ancestor_config_file = _find_class_config_file(
346                                $ancestor_class,
347                                $config_file,
348                                $config_dirs
349                        );
350
351                        # Skip if this is the primary config file - it will be added at the end
352
74
99
                        if ($ancestor_config_file && $ancestor_config_file eq $config_file) {
353
8
8
                                next;
354                        }
355
356                        # Only add if we found a file and haven't already added it
357
66
139
                        if ($ancestor_config_file && -r $ancestor_config_file && !$tracked_files{$ancestor_config_file}) {
358
19
30
                                push @config_files_to_load, {
359                                        file => $ancestor_config_file,
360                                        class => $ancestor_class
361                                };
362
19
18
                                $tracked_files{$ancestor_config_file} = 1;
363
364                                # Track for hot reload
365
19
55
                                if (-f $ancestor_config_file) {
366
19
29
                                        $_config_file_stats{$ancestor_config_file} = stat($ancestor_config_file);
367                                }
368                        }
369                }
370
371                # Ensure the primary config file is included LAST (highest priority)
372                # This handles the case where the primary file doesn't match the class name pattern
373
30
304
                if ($config_file && !$tracked_files{$config_file} && -r $config_file) {
374
27
53
                        push @config_files_to_load, {
375                                file => $config_file,
376                                class => $original_class
377                        };
378
27
37
                        $tracked_files{$config_file} = 1;
379
380
27
75
                        if (-f $config_file) {
381
27
54
                                $_config_file_stats{$config_file} = stat($config_file);
382                        }
383                }
384
385
30
2124
                if(!scalar(@config_files_to_load)) {
386                        # Can't find an inheritence tree
387
0
0
0
0
                        foreach my $dir(@{$config_dirs}) {
388
0
0
                                my $candidate = File::Spec->catfile($dir, $config_file);
389
0
0
                                if(-r $candidate) {
390
0
0
                                        push @config_files_to_load, {
391                                                file => $candidate,
392                                                class => $original_class
393                                        }
394                                }
395                        }
396                }
397        }
398
399        # Load and merge configurations from all files
400
40
80
        if (@config_files_to_load) {
401                # Sort by class hierarchy to ensure correct order (base -> parent -> child)
402                # This must happen AFTER all files are collected
403
30
32
                if (@config_files_to_load) {
404
30
26
                        my %class_order;
405
30
51
                        for my $i (0..$#inheritance_chain) {
406
74
73
                                $class_order{$inheritance_chain[$i]} = $i;
407                        }
408                        @config_files_to_load = sort {
409
30
21
55
47
                                ($class_order{$a->{class}} // 999) <=> ($class_order{$b->{class}} // 999)
410                        } @config_files_to_load;
411                }
412
413                # Start with the passed-in defaults
414
30
48
                my $merged_params = { %$params };
415
416
30
31
                foreach my $config_info (@config_files_to_load) {
417
46
153
                        my $cfg_file = $config_info->{file};
418
46
35
                        my $cfg_class = $config_info->{class};
419
46
30
                        my $section_name = $cfg_class;
420
46
59
                        $section_name =~ s/::/__/g;
421
422                        # When loading individual config files for inheritance,
423                        # don't pass config_dirs - just load the specific file
424
46
162
                        my $config = Config::Abstraction->new(
425                                config_file => $cfg_file,
426                                env_prefix => "${section_name}__"
427                        );
428
429
46
75386
                        if ($config) {
430                                # Get this config file's values for the section
431
46
85
                                my $this_config = $config->merge_defaults(
432                                        defaults => {},
433                                        section => $section_name,
434                                        merge => 1,
435                                        deep => 1
436                                );
437
438                                # Deep merge: later configs override earlier ones
439
46
2886
                                $merged_params = _deep_merge($merged_params, $this_config);
440                        } elsif ($@) {
441
0
0
                                carp("Warning: Can't load configuration from $cfg_file: $@");
442                        }
443                }
444
445
30
243
                $params = $merged_params;
446        } elsif (my $config = Config::Abstraction->new(env_prefix => "${class}__")) {
447                # Handle environment variables with inheritance
448
3
2661
                my $merged_config = {};
449
450                # Merge ancestor configurations from environment
451
3
3
                foreach my $ancestor_class (reverse @inheritance_chain) {
452
6
25
                        my $section_name = $ancestor_class;
453
6
10
                        $section_name =~ s/::/__/g;
454
455
6
11
                        my $ancestor_env_config = Config::Abstraction->new(
456                                env_prefix => "${section_name}__"
457                        );
458
459
6
2491
                        if ($ancestor_env_config) {
460
3
7
                                my $ancestor_config = $ancestor_env_config->merge_defaults(
461                                        defaults => {},
462                                        section => $section_name,
463                                        merge => 1,
464                                        deep => 1
465                                );
466
3
232
                                $merged_config = _deep_merge($merged_config, $ancestor_config);
467                        }
468                }
469
470
3
8
                $params = $config->merge_defaults(
471                        defaults => $params,
472                        section => $class,
473                        merge => 1,
474                        deep => 1
475                );
476
477                # Apply inherited config
478
3
154
                $params = _deep_merge($merged_config, $params);
479
480                # Track this config file for hot reload
481
3
10
                if ($params->{config_path} && -f $params->{config_path}) {
482
0
0
                        $_config_file_stats{$params->{config_path}} = stat($params->{config_path});
483                }
484        }
485
486
40
6254
        my $croak_on_error = exists($params->{'croak_on_error'}) ? $params->{'croak_on_error'} : 1;
487
40
50
        my $carp_on_warn = exists($params->{'carp_on_warn'}) ? $params->{'carp_on_warn'} : 0;
488
489        # Load the default logger
490
40
56
        if (my $logger = $params->{'logger'}) {
491
8
15
                if ($params->{'logger'} ne 'NULL') {
492
8
12
                        if(ref($logger) eq 'HASH') {
493
7
7
                                if ($logger->{'syslog'}) {
494                                        $params->{'logger'} = Log::Abstraction->new({
495                                                carp_on_warn => $carp_on_warn,
496                                                syslog => $logger->{'syslog'},
497
1
1
1
4
                                                %{$logger}
498                                        });
499                                } else {
500                                        $params->{'logger'} = Log::Abstraction->new({
501                                                carp_on_warn => $carp_on_warn,
502
6
6
11
32
                                                %{$logger}
503                                        });
504                                }
505                        } elsif(!blessed($logger) || !$logger->isa('Log::Abstraction')) {
506
1
4
                                $params->{'logger'} = Log::Abstraction->new({
507                                        carp_on_warn => $carp_on_warn,
508                                        logger => $logger
509                                });
510                        }
511                }
512        } elsif ($array) {
513
0
0
                $params->{'logger'} = Log::Abstraction->new(
514                        array => $array,
515                        carp_on_warn => $carp_on_warn
516                );
517
0
0
                undef $array;
518        } else {
519
32
112
                $params->{'logger'} = Log::Abstraction->new(carp_on_warn => $carp_on_warn);
520        }
521
522
40
207579
        if ($array && !$params->{'logger'}->{'array'}) {
523
0
0
                $params->{'logger'}->{'array'} = $array;
524        }
525
526        # Store config file path in params for hot reload
527
40
61
        $params->{_config_file} = $config_file if(defined($config_file));
528
40
46
55
77
        $params->{_config_files} = [map { $_->{file} } @config_files_to_load] if @config_files_to_load;
529
530
40
115
        return Return::Set::set_return($params, { 'type' => 'hashref' });
531}
532
533# Find the appropriate config file for a given class
534# Looks for class-specific config files based on naming conventions
535sub _find_class_config_file {
536
75
706
        my ($class, $base_config_file, $config_dirs) = @_;
537
538        # Convert class name to file-friendly format
539
75
61
        my $class_file = lc($class);
540
75
88
        $class_file =~ s/::/-/g;
541
542        # Extract directory, basename, and extension from base config file
543
75
48
        my ($base_dir, $base_name, $base_ext);
544
545
75
254
        if ($base_config_file =~ m{^(.*/)([^/]+?)(\.[^.]+)?$}) {
546
68
110
                $base_dir = $1 || '';
547
68
56
                $base_name = $2;
548
68
81
                $base_ext = $3 || '';
549        } else {
550
7
5
                $base_name = $base_config_file;
551
7
5
                $base_dir = '';
552
7
3
                $base_ext = '';
553        }
554
555        # Try several naming patterns
556
75
146
        my @patterns = (
557                "${base_dir}${class_file}${base_ext}",           # my-parent-class.yml
558                "${base_dir}${class_file}.conf",                  # my-parent-class.conf
559                "${base_dir}${class_file}.yml",                   # my-parent-class.yml
560                "${base_dir}${class_file}.yaml",                  # my-parent-class.yaml
561                "${base_dir}${class_file}.json",                  # my-parent-class.json
562        );
563
564        # Also try with config_dirs if provided
565
75
124
        if ($config_dirs && ref($config_dirs) eq 'ARRAY') {
566
37
31
                foreach my $dir (@$config_dirs) {
567                        # Remove trailing slash if present
568
37
27
                        $dir =~ s{/$}{};
569
37
64
                        push @patterns, (
570                                "${dir}/${class_file}${base_ext}",
571                                "${dir}/${class_file}.conf",
572                                "${dir}/${class_file}.yml",
573                                "${dir}/${class_file}.yaml",
574                                "${dir}/${class_file}.json",
575                        );
576                }
577        }
578
579        # Return the first file that exists and is readable
580
75
60
        foreach my $pattern (@patterns) {
581
357
1089
                if (-r $pattern && -f $pattern) {
582
28
67
                        return $pattern;
583                }
584        }
585
586
47
72
        return undef;
587}
588
589# Helper function to get the inheritance chain for a class
590sub _get_inheritance_chain {
591
43
76414
        my ($class) = @_;
592
43
45
        my @chain = ();
593
43
33
        my %seen = ();
594
595
43
78
        _walk_isa($class, \@chain, \%seen);
596
597
43
80
        return @chain;
598}
599
600# Recursive function to walk the @ISA hierarchy
601sub _walk_isa {
602
102
89
        my ($class, $chain, $seen) = @_;
603
604
102
126
        return if $seen->{$class}++;
605
606        # Get the @ISA array for this class
607
11
11
11
35
10
282
        no strict 'refs';
608
102
102
64
188
        my @isa = @{"${class}::ISA"};
609
11
11
11
17
15
7307
        use strict 'refs';
610
611        # Recursively process parent classes first
612
102
93
        foreach my $parent (@isa) {
613                # Skip common base classes that won't have configs
614                # next if $parent eq 'Exporter';
615                # next if $parent eq 'DynaLoader';
616                # next if $parent eq 'UNIVERSAL';
617
618
16
14
                _walk_isa($parent, $chain, $seen);
619        }
620
621        # If this class has no parents and isn't UNIVERSAL itself,
622        # explicitly add UNIVERSAL as a parent
623
102
164
        if (!@isa && $class ne 'UNIVERSAL') {
624
43
51
                _walk_isa('UNIVERSAL', $chain, $seen);
625        }
626
627        # Add current class to chain (after parents)
628
102
101
        push @$chain, $class;
629}
630
631# Deep merge two hash references
632# Second hash takes precedence over first
633sub _deep_merge {
634
54
46
        my ($base, $overlay) = @_;
635
636
54
74
        return $overlay unless ref($base) eq 'HASH';
637
54
59
        return $base unless ref($overlay) eq 'HASH';
638
639
54
78
        my $result = { %$base };
640
641
54
66
        foreach my $key (keys %$overlay) {
642
99
120
                if (ref($overlay->{$key}) eq 'HASH' && ref($result->{$key}) eq 'HASH') {
643
2
6
                        $result->{$key} = _deep_merge($result->{$key}, $overlay->{$key});
644                } else {
645
97
101
                        $result->{$key} = $overlay->{$key};
646                }
647        }
648
649
54
217
        return $result;
650}
651
652
653 - 658
=head2 instantiate($class,...)

Create and configure an object of the given class.
This is a quick and dirty way of making third-party classes configurable at runtime.

=cut
659
660sub instantiate
661{
662
3
78789
        my $params = Params::Get::get_params('class', @_);
663
664
3
39
        my $class = $params->{'class'};
665
3
4
        $params = configure($class, $params);
666
667
3
230
        my $obj = $class->new($params);
668
669        # Register object for hot reload if config file is used
670
3
12
        if ($params->{_config_file}) {
671
2
3
                register_object($class, $obj);
672        }
673
674
3
10
        return $obj;
675}
676
677 - 688
=head1 HOT RELOAD FEATURES

=head2 enable_hot_reload

Enable hot reloading for configuration files.

    Object::Configure::enable_hot_reload(
        interval => 5,  # Check every 5 seconds (default: 10)
        callback => sub { print "Config reloaded!\n"; }  # Optional callback
    );

=cut
689
690sub enable_hot_reload {
691
2
3558
        my %params = @_;
692
693
2
4
        my $interval = $params{interval} || 10;
694
2
4
        my $callback = $params{callback};
695
696        # Don't start multiple watchers
697
2
4
        return if %_config_watchers;
698
699        # Fork a background process to watch config files
700
2
2092
        if (my $pid = fork()) {
701                # Parent process - store the watcher PID
702
1
33
                $_config_watchers{pid} = $pid;
703
1
22
                $_config_watchers{callback} = $callback;
704
1
27
                return $pid;
705        } elsif (defined $pid) {
706                # Child process - run the file watcher
707
1
25
                _run_config_watcher($interval, $callback);
708
0
0
                exit 0;
709        } else {
710
0
0
                croak("Failed to fork config watcher: $!");
711        }
712}
713
714 - 720
=head2 disable_hot_reload

Disable hot reloading and stop the background watcher.

    Object::Configure::disable_hot_reload();

=cut
721
722sub disable_hot_reload {
723
14
2917
        if (my $pid = $_config_watchers{pid}) {
724
1
11
                kill('TERM', $pid);
725
1
174672
                waitpid($pid, 0);
726
1
11
                %_config_watchers = ();
727        }
728}
729
730 - 736
=head2 reload_config

Manually trigger a configuration reload for all registered objects.

    Object::Configure::reload_config();

=cut
737
738sub reload_config {
739
12
72339
        my $reloaded_count = 0;
740
741
12
31
        foreach my $class_key (keys %_object_registry) {
742
12
15
                my $objects = $_object_registry{$class_key};
743
744                # Clean up dead object references
745
12
70
22
72
                @$objects = grep { defined $_ } @$objects;
746
747
12
13
                foreach my $obj_ref (@$objects) {
748
70
64
                        if (my $obj = $$obj_ref) {
749
13
15
                                eval {
750
13
15
                                        _reload_object_config($obj);
751
13
310
                                        $reloaded_count++;
752                                };
753
13
19
                                if ($@) {
754
0
0
                                        warn "Failed to reload config for object: $@";
755                                }
756                        }
757                }
758
759                # Remove empty entries
760
12
18
                delete $_object_registry{$class_key} unless @$objects;
761        }
762
763
12
17
        return $reloaded_count;
764}
765
766# Internal function to run the config file watcher
767sub _run_config_watcher {
768
1
9
        my ($interval, $callback) = @_;
769
770        # Set up signal handlers for clean shutdown
771
1
1
63
47
        local $SIG{TERM} = sub { exit 0 };
772
1
0
26
0
        local $SIG{INT} = sub { exit 0 };
773
774
1
3
        while (1) {
775
3
2003378
                sleep($interval);
776
777
3
44
                my $changes_detected = 0;
778
779                # Check each monitored config file
780
2
31
                foreach my $config_file (keys %_config_file_stats) {
781
2
56
                        if (-f $config_file) {
782
2
44
                                my $current_stat = stat($config_file);
783
2
435
                                my $stored_stat = $_config_file_stats{$config_file};
784
785                                # Compare modification times
786
2
44
                                if ((!$stored_stat) || ($current_stat->mtime > $stored_stat->mtime)) {
787
1
10
                                        $_config_file_stats{$config_file} = $current_stat;
788
1
6
                                        $changes_detected = 1;
789                                }
790                        } else {
791                                # File was deleted
792
0
0
                                delete $_config_file_stats{$config_file};
793
0
0
                                $changes_detected = 1;
794                        }
795                }
796
797
2
17
                if($changes_detected) {
798
1
16
                        if($^O ne 'MSWin32') {
799                                # Reload configurations in the main process
800                                # Use a signal or shared memory mechanism
801
1
17
                                if(my $parent_pid = getppid()) {
802
1
20
                                        kill('USR1', $parent_pid);
803                                }
804                        }
805                }
806        }
807}
808
809# Internal function to reload a single object's configuration
810sub _reload_object_config {
811
13
16
        my ($obj) = @_;
812
813
13
31
        return unless blessed($obj);
814
815
13
16
        my $class = ref($obj);
816
13
9
        my $original_class = $class;
817
13
18
        $class =~ s/::/__/g;
818
819        # Get the original config file path if it exists
820
13
21
        my $config_file = $obj->{_config_file} || $obj->{config_file};
821
13
98
        return unless $config_file && -f $config_file;
822
823        # Reload the configuration
824
12
61
        my $config = Config::Abstraction->new(
825                config_file => $config_file,
826                env_prefix => "${class}__"
827        );
828
829
12
17786
        if ($config) {
830                # Use merge_defaults with empty defaults to get just the config values
831
12
22
                my $new_params = $config->merge_defaults(
832                        defaults => {},
833                        section => $class,
834                        merge => 1,
835                        deep => 1
836                );
837
838                # Update object properties, preserving non-config data
839
12
609
                foreach my $key (keys %$new_params) {
840
13
18
                        next if $key =~ /^_/;   # Skip private properties
841
842
13
25
                        if($key =~ /^logger/ && $new_params->{$key} ne 'NULL') {
843                                # Handle logger reconfiguration specially
844
1
2
                                _reconfigure_logger($obj, $key, $new_params->{$key});
845                        } else {
846
12
15
                                $obj->{$key} = $new_params->{$key};
847                        }
848                }
849
850                # Call object's reload hook if it exists
851
12
27
                if ($obj->can('_on_config_reload')) {
852
12
22
                        $obj->_on_config_reload($new_params);
853                }
854
855                # Log the reload if logger exists
856
12
93
                if ($obj->{logger} && $obj->{logger}->can('info')) {
857
12
29
                        $obj->{logger}->info("Configuration reloaded for $original_class");
858                }
859        }
860}
861
862# Internal function to reconfigure the logger
863sub _reconfigure_logger
864{
865
1
2
        my ($obj, $key, $logger_config) = @_;
866
867
1
2
        if (ref($logger_config) eq 'HASH') {
868                # Create new logger with new config
869
0
0
                my $carp_on_warn = $obj->{carp_on_warn} || 0;
870
871
0
0
                if ($logger_config->{syslog}) {
872                        $obj->{$key} = Log::Abstraction->new({
873                                carp_on_warn => $carp_on_warn,
874                                syslog => $logger_config->{syslog},
875
0
0
                                %$logger_config
876                        });
877                } else {
878
0
0
                        $obj->{$key} = Log::Abstraction->new({
879                                carp_on_warn => $carp_on_warn,
880                                %$logger_config
881                        });
882                }
883        } else {
884
1
2
                $obj->{$key} = $logger_config;
885        }
886}
887
888 - 911
=head2 register_object($class, $obj)

Register an object for hot reload monitoring.

    Object::Configure::register_object($class, $obj);

This is automatically called by the configure() function when a config file is used,
but can also be called manually to register objects for hot reload.

=head3 Parameters

=over 4

=item * C<class> (Required)

The class of the object, used for the configuration name.

=item * C<obj> (Required)

The object to be configured

=back

=cut
912
913sub register_object {
914
17
1159
        my ($class, $obj) = @_;
915
916
17
38
        croak(__PACKAGE__, '::register_object: Usage ($class, $obj)') unless(defined($class) && defined($obj));
917
918        # Use weak references to avoid memory leaks
919
17
14
        my $obj_ref = \$obj;
920
17
27
        weaken($$obj_ref);
921
922
17
17
32
23
        push @{$_object_registry{$class}}, $obj_ref;
923
924        # Set up signal handler for hot reload (only once)
925
17
34
        if (!defined $_original_usr1_handler) {
926                # Store the existing handler (could be DEFAULT, IGNORE, or a code ref)
927
6
22
                $_original_usr1_handler = $SIG{USR1} || 'DEFAULT';
928
929
6
17
                return if($^O eq 'MSWin32');    # There is no SIGUSR1 on Windows
930
931                $SIG{USR1} = sub {
932                        # Handle our hot reload first
933
3
1016528
                        reload_config();
934
3
6
                        if ($_config_watchers{callback}) {
935
1
4
                                $_config_watchers{callback}->();
936                        }
937
938                        # Chain to the original handler if it exists and is callable
939
3
7
                        if (ref($_original_usr1_handler) eq 'CODE') {
940
3
5
                                $_original_usr1_handler->();
941                        } elsif ($_original_usr1_handler eq 'DEFAULT') {
942                                # Let the default handler run (which typically does nothing for USR1)
943                                # We don't need to explicitly call it
944                        } elsif ($_original_usr1_handler eq 'IGNORE') {
945                                # Do nothing - the signal was being ignored
946                        }
947                        # Note: If it was some other string, it was probably a custom handler name
948                        # but we can't easily call those, so we'll just warn
949                        elsif ($_original_usr1_handler ne 'DEFAULT' && $_original_usr1_handler ne 'IGNORE') {
950
0
0
                                warn "Object::Configure: Cannot chain to non-code USR1 handler: $_original_usr1_handler";
951                        }
952
6
47
                };
953        }
954}
955
956 - 963
=head2 restore_signal_handlers

Restore original signal handlers and disable hot reload integration.
Useful when you want to cleanly shut down the hot reload system.

    Object::Configure::restore_signal_handlers();

=cut
964
965sub restore_signal_handlers
966{
967
17
205095
        if (defined $_original_usr1_handler) {
968
6
41
                $SIG{USR1} = $_original_usr1_handler if($^O ne 'MSWin32');      # There is no SIGUSR1 on Windows
969
6
44
                $_original_usr1_handler = undef;
970        }
971}
972
973 - 982
=head2 get_signal_handler_info

Get information about the current signal handler setup.
Useful for debugging signal handler chains.

    my $info = Object::Configure::get_signal_handler_info();
    print "Original USR1 handler: ", $info->{original_usr1} || 'none', "\n";
    print "Hot reload active: ", $info->{hot_reload_active} ? 'yes' : 'no', "\n";

=cut
983
984sub get_signal_handler_info {
985        return {
986                original_usr1 => $_original_usr1_handler,
987                current_usr1 => $SIG{USR1},
988                hot_reload_active => defined $_original_usr1_handler,
989                watcher_pid => $_config_watchers{pid},
990
2
12
        };
991}
992
993# Cleanup on module destruction
994END {
995
11
88804
        disable_hot_reload();
996
997        # Restore original USR1 handler if we modified it
998
11
52
        restore_signal_handlers();
999}
1000
1001 - 1045
=head1 SEE ALSO

=over 4

=item * L<Config::Abstraction>

=item * L<Log::Abstraction>

=item * Test coverage report: L<https://nigelhorne.github.io/Object-Configure/coverage/>

=back

=head1 SUPPORT

This module is provided as-is without any warranty.

Please report any bugs or feature requests to C<bug-object-configure at rt.cpan.org>,
or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Object-Configure>.
I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

You can find documentation for this module with the perldoc command.

    perldoc Object::Configure

=head1 LICENSE AND COPYRIGHT

Copyright 2025 Nigel Horne.

Usage is subject to licence terms.

The licence terms of this software are as follows:

=over 4

=item * Personal single user, single computer use: GPL2

=item * All other users (including Commercial, Charity, Educational, Government)
  must apply in writing for a licence for use from Nigel Horne at the
  above e-mail.

=back

=cut
1046
10471;