File Coverage

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

linestmtbrancondsubtimecode
1package Object::Configure;
2
3
11
11
11
839670
10
146
use strict;
4
11
11
11
19
6
190
use warnings;
5
6
11
11
11
24
8
259
use Carp;
7
11
11
11
2310
317993
162
use Config::Abstraction 0.37;
8
11
11
11
30
6
153
use File::Spec;
9
11
11
11
2490
135223
189
use Log::Abstraction 0.26;
10
11
11
11
24
50
167
use Params::Get 0.13;
11
11
11
11
21
19
138
use Return::Set;
12
11
11
11
20
6
199
use Scalar::Util qw(blessed weaken);
13
11
11
11
15
8
44
use Time::HiRes qw(time);
14
11
11
11
3005
30196
8055
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
2249471
        my $class = $_[0];
313
42
89
        my $params = $_[1] || {};       # Contains the defaults, the run time config will overwrite them
314
42
61
        my $array;
315
316
42
72
        croak(__PACKAGE__, ': configure: what class do you want to configure?') if(!defined($class));
317
318
42
96
        if(exists($params->{'logger'}) && (ref($params->{'logger'}) eq 'ARRAY')) {
319
0
0
                $array = delete $params->{'logger'};
320        }
321
322
42
43
        my $original_class = $class;
323
42
96
        $class =~ s/::/__/g;
324
325        # Store config file path for hot reload
326
42
49
        my $config_file = $params->{'config_file'};
327
42
55
        my $config_dirs = $params->{'config_dirs'};
328
329        # Get inheritance chain for finding ancestor config files
330
42
70
        my @inheritance_chain = _get_inheritance_chain($original_class);
331
332        # Build list of config files to load (ancestor to child order)
333
42
39
        my @config_files_to_load = ();
334
42
47
        my %tracked_files = ();
335
336
42
76
        if ($config_file) {
337                # Check if primary config file is readable (unless config_dirs provided)
338
32
206
                if ((!$config_dirs) && (!-r $config_file)) {
339
2
45
                        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
44
                foreach my $ancestor_class (reverse @inheritance_chain) {
345
74
1027
                        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
92
                        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
144
                        if ($ancestor_config_file && -r $ancestor_config_file && !$tracked_files{$ancestor_config_file}) {
358
19
29
                                push @config_files_to_load, {
359                                        file => $ancestor_config_file,
360                                        class => $ancestor_class
361                                };
362
19
34
                                $tracked_files{$ancestor_config_file} = 1;
363
364                                # Track for hot reload
365
19
51
                                if (-f $ancestor_config_file) {
366
19
23
                                        $_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
347
                if ($config_file && !$tracked_files{$config_file} && -r $config_file) {
374
27
59
                        push @config_files_to_load, {
375                                file => $config_file,
376                                class => $original_class
377                        };
378
27
45
                        $tracked_files{$config_file} = 1;
379
380
27
104
                        if (-f $config_file) {
381
27
57
                                $_config_file_stats{$config_file} = stat($config_file);
382                        }
383                }
384
385
30
2163
                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
102
        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
35
                if (@config_files_to_load) {
404
30
19
                        my %class_order;
405
30
47
                        for my $i (0..$#inheritance_chain) {
406
74
69
                                $class_order{$inheritance_chain[$i]} = $i;
407                        }
408                        @config_files_to_load = sort {
409
30
21
69
39
                                ($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
51
                my $merged_params = { %$params };
415
416
30
27
                foreach my $config_info (@config_files_to_load) {
417
46
153
                        my $cfg_file = $config_info->{file};
418
46
27
                        my $cfg_class = $config_info->{class};
419
46
35
                        my $section_name = $cfg_class;
420
46
92
                        $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
181
                        my $config = Config::Abstraction->new(
425                                config_file => $cfg_file,
426                                env_prefix => "${section_name}__"
427                        );
428
429
46
78324
                        if ($config) {
430                                # Get this config file's values for the section
431
46
87
                                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
2853
                                $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
245
                $params = $merged_params;
446        } elsif (my $config = Config::Abstraction->new(env_prefix => "${class}__")) {
447                # Handle environment variables with inheritance
448
3
2834
                my $merged_config = {};
449
450                # Merge ancestor configurations from environment
451
3
4
                foreach my $ancestor_class (reverse @inheritance_chain) {
452
6
27
                        my $section_name = $ancestor_class;
453
6
9
                        $section_name =~ s/::/__/g;
454
455
6
12
                        my $ancestor_env_config = Config::Abstraction->new(
456                                env_prefix => "${section_name}__"
457                        );
458
459
6
2660
                        if ($ancestor_env_config) {
460
3
9
                                my $ancestor_config = $ancestor_env_config->merge_defaults(
461                                        defaults => {},
462                                        section => $section_name,
463                                        merge => 1,
464                                        deep => 1
465                                );
466
3
243
                                $merged_config = _deep_merge($merged_config, $ancestor_config);
467                        }
468                }
469
470
3
12
                $params = $config->merge_defaults(
471                        defaults => $params,
472                        section => $class,
473                        merge => 1,
474                        deep => 1
475                );
476
477                # Apply inherited config
478
3
175
                $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
6748
        my $croak_on_error = exists($params->{'croak_on_error'}) ? $params->{'croak_on_error'} : 1;
487
40
63
        my $carp_on_warn = exists($params->{'carp_on_warn'}) ? $params->{'carp_on_warn'} : 0;
488
489        # Load the default logger
490
40
66
        if (my $logger = $params->{'logger'}) {
491
8
13
                if ($params->{'logger'} ne 'NULL') {
492
8
13
                        if(ref($logger) eq 'HASH') {
493
7
17
                                if(exists $logger->{'syslog'}) {
494                                        $params->{'logger'} = Log::Abstraction->new({
495                                                carp_on_warn => $carp_on_warn,
496                                                syslog => $logger->{'syslog'},
497
1
1
2
3
                                                %{$logger}
498                                        });
499                                } else {
500                                        $params->{'logger'} = Log::Abstraction->new({
501                                                carp_on_warn => $carp_on_warn,
502
6
6
5
30
                                                %{$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
131
                $params->{'logger'} = Log::Abstraction->new(carp_on_warn => $carp_on_warn);
520        }
521
522
40
214957
        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
73
        $params->{_config_file} = $config_file if(defined($config_file));
528
40
46
71
72
        $params->{_config_files} = [map { $_->{file} } @config_files_to_load] if @config_files_to_load;
529
530
40
138
        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
657
        my ($class, $base_config_file, $config_dirs) = @_;
537
538        # Convert class name to file-friendly format
539
75
71
        my $class_file = lc($class);
540
75
78
        $class_file =~ s/::/-/g;
541
542        # Extract directory, basename, and extension from base config file
543
75
56
        my ($base_dir, $base_name, $base_ext);
544
545
75
268
        if ($base_config_file =~ m{^(.*/)([^/]+?)(\.[^.]+)?$}) {
546
68
98
                $base_dir = $1 || '';
547
68
67
                $base_name = $2;
548
68
89
                $base_ext = $3 || '';
549        } else {
550
7
3
                $base_name = $base_config_file;
551
7
4
                $base_dir = '';
552
7
5
                $base_ext = '';
553        }
554
555        # Try several naming patterns
556
75
155
        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
97
        if ($config_dirs && ref($config_dirs) eq 'ARRAY') {
566
37
22
                foreach my $dir (@$config_dirs) {
567                        # Remove trailing slash if present
568
37
32
                        $dir =~ s{/$}{};
569
37
55
                        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
81
        foreach my $pattern (@patterns) {
581
357
1101
                if (-r $pattern && -f $pattern) {
582
28
40
                        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
78402
        my ($class) = @_;
592
43
42
        my @chain = ();
593
43
45
        my %seen = ();
594
595
43
95
        _walk_isa($class, \@chain, \%seen);
596
597
43
88
        return @chain;
598}
599
600# Recursive function to walk the @ISA hierarchy
601sub _walk_isa {
602
102
88
        my ($class, $chain, $seen) = @_;
603
604
102
141
        return if $seen->{$class}++;
605
606        # Get the @ISA array for this class
607
11
11
11
40
5
277
        no strict 'refs';
608
102
102
61
189
        my @isa = @{"${class}::ISA"};
609
11
11
11
27
7
7826
        use strict 'refs';
610
611        # Recursively process parent classes first
612
102
94
        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
13
                _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
175
        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
105
        push @$chain, $class;
629}
630
631# Deep merge two hash references
632# Second hash takes precedence over first
633sub _deep_merge {
634
54
52
        my ($base, $overlay) = @_;
635
636
54
81
        return $overlay unless ref($base) eq 'HASH';
637
54
59
        return $base unless ref($overlay) eq 'HASH';
638
639
54
87
        my $result = { %$base };
640
641
54
68
        foreach my $key (keys %$overlay) {
642
99
132
                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
85
                        $result->{$key} = $overlay->{$key};
646                }
647        }
648
649
54
206
        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
80471
        my $params = Params::Get::get_params('class', @_);
663
664
3
38
        my $class = $params->{'class'};
665
3
8
        $params = configure($class, $params);
666
667
3
266
        my $obj = $class->new($params);
668
669        # Register object for hot reload if config file is used
670
3
10
        if ($params->{_config_file}) {
671
2
3
                register_object($class, $obj);
672        }
673
674
3
9
        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
3658
        my %params = @_;
692
693
2
4
        my $interval = $params{interval} || 10;
694
2
2
        my $callback = $params{callback};
695
696        # Don't start multiple watchers
697
2
6
        return if %_config_watchers;
698
699        # Fork a background process to watch config files
700
2
2356
        if (my $pid = fork()) {
701                # Parent process - store the watcher PID
702
1
47
                $_config_watchers{pid} = $pid;
703
1
27
                $_config_watchers{callback} = $callback;
704
1
57
                return $pid;
705        } elsif (defined $pid) {
706                # Child process - run the file watcher
707
1
47
                _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
3306
        if (my $pid = $_config_watchers{pid}) {
724
1
14
                kill('TERM', $pid);
725
1
192025
                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
72459
        my $reloaded_count = 0;
740
741
12
29
        foreach my $class_key (keys %_object_registry) {
742
12
16
                my $objects = $_object_registry{$class_key};
743
744                # Clean up dead object references
745
12
70
18
75
                @$objects = grep { defined $_ } @$objects;
746
747
12
14
                foreach my $obj_ref (@$objects) {
748
70
67
                        if (my $obj = $$obj_ref) {
749
13
13
                                eval {
750
13
16
                                        _reload_object_config($obj);
751
13
323
                                        $reloaded_count++;
752                                };
753
13
17
                                if ($@) {
754
0
0
                                        warn "Failed to reload config for object: $@";
755                                }
756                        }
757                }
758
759                # Remove empty entries
760
12
16
                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
8
        my ($interval, $callback) = @_;
769
770        # Set up signal handlers for clean shutdown
771
1
1
121
49
        local $SIG{TERM} = sub { exit 0 };
772
1
0
46
0
        local $SIG{INT} = sub { exit 0 };
773
774
1
7
        while (1) {
775
3
2003748
                sleep($interval);
776
777
3
56
                my $changes_detected = 0;
778
779                # Check each monitored config file
780
2
34
                foreach my $config_file (keys %_config_file_stats) {
781
2
67
                        if (-f $config_file) {
782
2
80
                                my $current_stat = stat($config_file);
783
2
469
                                my $stored_stat = $_config_file_stats{$config_file};
784
785                                # Compare modification times
786
2
52
                                if ((!$stored_stat) || ($current_stat->mtime > $stored_stat->mtime)) {
787
1
22
                                        $_config_file_stats{$config_file} = $current_stat;
788
1
11
                                        $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
20
                if($changes_detected) {
798
1
11
                        if($^O ne 'MSWin32') {
799                                # Reload configurations in the main process
800                                # Use a signal or shared memory mechanism
801
1
19
                                if(my $parent_pid = getppid()) {
802
1
24
                                        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
14
        my ($obj) = @_;
812
813
13
33
        return unless blessed($obj);
814
815
13
17
        my $class = ref($obj);
816
13
12
        my $original_class = $class;
817
13
31
        $class =~ s/::/__/g;
818
819        # Get the original config file path if it exists
820
13
24
        my $config_file = $obj->{_config_file} || $obj->{config_file};
821
13
121
        return unless $config_file && -f $config_file;
822
823        # Reload the configuration
824
12
52
        my $config = Config::Abstraction->new(
825                config_file => $config_file,
826                env_prefix => "${class}__"
827        );
828
829
12
18770
        if ($config) {
830                # Use merge_defaults with empty defaults to get just the config values
831
12
25
                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
601
                foreach my $key (keys %$new_params) {
840
13
22
                        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
3
                                _reconfigure_logger($obj, $key, $new_params->{$key});
845                        } else {
846
12
16
                                $obj->{$key} = $new_params->{$key};
847                        }
848                }
849
850                # Call object's reload hook if it exists
851
12
24
                if ($obj->can('_on_config_reload')) {
852
12
30
                        $obj->_on_config_reload($new_params);
853                }
854
855                # Log the reload if logger exists
856
12
96
                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
1
        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
1255
        my ($class, $obj) = @_;
915
916
17
42
        croak(__PACKAGE__, '::register_object: Usage ($class, $obj)') unless(defined($class) && defined($obj));
917
918        # Use weak references to avoid memory leaks
919
17
17
        my $obj_ref = \$obj;
920
17
37
        weaken($$obj_ref);
921
922
17
17
15
25
        push @{$_object_registry{$class}}, $obj_ref;
923
924        # Set up signal handler for hot reload (only once)
925
17
48
        if (!defined $_original_usr1_handler) {
926                # Store the existing handler (could be DEFAULT, IGNORE, or a code ref)
927
6
33
                $_original_usr1_handler = $SIG{USR1} || 'DEFAULT';
928
929
6
18
                return if($^O eq 'MSWin32');    # There is no SIGUSR1 on Windows
930
931                $SIG{USR1} = sub {
932                        # Handle our hot reload first
933
3
1014818
                        reload_config();
934
3
12
                        if ($_config_watchers{callback}) {
935
1
3
                                $_config_watchers{callback}->();
936                        }
937
938                        # Chain to the original handler if it exists and is callable
939
3
8
                        if (ref($_original_usr1_handler) eq 'CODE') {
940
3
6
                                $_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
61
                };
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
205113
        if (defined $_original_usr1_handler) {
968
6
43
                $SIG{USR1} = $_original_usr1_handler if($^O ne 'MSWin32');      # There is no SIGUSR1 on Windows
969
6
22
                $_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
14
        };
991}
992
993# Cleanup on module destruction
994END {
995
11
93948
        disable_hot_reload();
996
997        # Restore original USR1 handler if we modified it
998
11
25
        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;