| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- package TPSGI::Startup;
- # ABSTRACT: What bin/tarbaby needs before TPSGI is loaded: the configuration, and the inotify watches
- # Lightweight startup helpers used by bin/tarbaby before the full TPSGI.pm
- # is loaded into workers. Keeping these separate means updating TPSGI.pm
- # doesn't require restarting tarbaby to pick up get_config / inotify changes.
- use 5.040;
- use warnings FATAL => 'all';
- use re '/aasx';
- use Cwd;
- use File::Basename qw{dirname};
- use File::Find;
- use List::Util qw{first};
- use Scalar::Util qw{refaddr};
- use Linux::Perl::inotify;
- use Config::Simple;
- our @wds;
- our $inotify;
- # wd => { path => ..., events => [...], restart => bool, callbacks => [ { key => ..., code => ... } ] }
- our %watches;
- # path => wd, so that we don't re-issue inotify_add_watch() needlessly.
- our %watched;
- # Whether watch_for_changes() has already set up the libdir watches.
- our $watching = 0;
- our @DEFAULT_EVENTS = qw{CREATE MODIFY DELETE MOVE};
- # Bit => name for every event we can actually be handed back by read().
- my %EVENT_NAMES = map { Linux::Perl::inotify::->EVENT_NUMBER()->{$_} => $_ } qw{
- ACCESS MODIFY ATTRIB CLOSE_WRITE CLOSE_NOWRITE OPEN
- MOVED_FROM MOVED_TO CREATE DELETE DELETE_SELF MOVE_SELF
- UNMOUNT Q_OVERFLOW IGNORED ISDIR
- };
- =head2 get_config()
- Read ~/.tpsgi.ini and return a merged options hash.
- =cut
- sub get_config {
- $ENV{HOME} ||= Cwd::getcwd();
- my %options = (
- verbose => 0,
- custom_log => undef,
- routers => [],
- loggers => [],
- auth => undef,
- domain => '',
- user => '',
- binds => [],
- basedir => '.',
- http_user => '',
- tpsgi_dir => Cwd::getcwd(),
- autoreload => 0,
- );
- my $config_file = "$ENV{HOME}/.tpsgi.ini";
- if ( -e $config_file ) {
- my $conf = Config::Simple->new($config_file);
- my %config;
- %config = %{ $conf->param( -block => 'default' ) } if $conf;
- foreach my $opt ( keys(%options) ) {
- if ( ref $options{$opt} eq 'ARRAY' ) {
- next unless exists $config{$opt};
- my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{ $config{$opt} } : ( $config{$opt} );
- push( @{ $options{$opt} }, @arrayed );
- next;
- }
- $options{$opt} = $config{$opt} if exists $config{$opt};
- }
- }
- my $LOGNAME = "$options{tpsgi_dir}/log/tpsgi.log";
- $LOGNAME = $options{custom_log} if $options{custom_log};
- my $LOGDIR = dirname($LOGNAME);
- $options{log_dir} = $LOGDIR;
- $options{log_name} = $LOGNAME;
- return %options;
- }
- =head2 watch_for_changes(@dirs)
- Set up inotify watches on the given directories (and any .pm files within them).
- These watches are flagged as 'restart' watches, which is what powers the
- autoreload feature (see TPSGI::handle_changes).
- Idempotent: no-op if the libdir watches are already active.
- =cut
- sub watch_for_changes {
- my (@dirs) = @_;
- return if $watching;
- $watching = 1;
- my @to_watch = @dirs;
- File::Find::find(
- {
- wanted => sub {
- my $object = $_;
- push( @to_watch, $object ) if ( !-d $object && $object =~ m/[.]pm$/ );
- },
- no_chdir => 1,
- bydepth => 1,
- },
- @dirs
- );
- foreach my $f2watch (@to_watch) {
- print "Watching $f2watch for changes\n";
- push( @wds, add_watch( path => $f2watch, restart => 1 ) );
- }
- return @wds;
- }
- =head2 add_watch(%args)
- Add an arbitrary file or directory to the watchlist, optionally with a callback
- to run when it changes. Returns the inotify watch descriptor.
- %args is:
- =over
- =item * C<path> - Required. File or directory to watch. When you watch a
- directory you will be told about changes to the entries within it (but not
- recursively); when you watch a file you are told about changes to that file.
- =item * C<callback> - Optional CODE reference, called as
- C<< $callback->($tpsgi, $change) >> by TPSGI::handle_changes() when the watched
- path changes. See that method for the shape of $change.
- =item * C<key> - Optional string identifying the callback. Registering the same
- key twice replaces the prior callback rather than stacking a second one.
- Defaults to the refaddr of the callback, which is what you want when you
- register the same named sub or package-level closure repeatedly (TPSGI::new()
- runs per request, so this happens constantly).
- =item * C<events> - Optional ARRAY reference of inotify events to watch for.
- Defaults to CREATE, MODIFY, DELETE and MOVE. If the path is already watched,
- any events not already in its mask are added to it.
- =item * C<restart> - Optional boolean. Marks this as a watch which should
- restart the server when a .pm within it changes, if autoreload is on.
- =back
- =cut
- sub add_watch {
- my (%args) = @_;
- my $path = $args{path};
- die "add_watch: path is required" if ( $path // '' ) eq '';
- die "add_watch: callback must be a CODE reference" if defined $args{callback} && ref $args{callback} ne 'CODE';
- die "add_watch: events must be an ARRAY reference" if defined $args{events} && ref $args{events} ne 'ARRAY';
- $inotify //= Linux::Perl::inotify::->new( flags => [qw{NONBLOCK}] );
- my $events = $args{events} || [@DEFAULT_EVENTS];
- my $wd = $watched{$path};
- if ( !defined $wd ) {
- $wd = $watched{$path} = $inotify->add( path => $path, events => $events );
- $watches{$wd} = {
- path => $path,
- events => [@$events],
- restart => 0,
- callbacks => [],
- };
- }
- elsif ( $args{events} ) {
- # inotify_add_watch() *replaces* the mask by default, so union it in instead.
- my %have = map { $_ => 1 } @{ $watches{$wd}{events} };
- my @new = grep { !$have{$_} } @$events;
- if (@new) {
- $inotify->add( path => $path, events => [ @new, 'MASK_ADD' ] );
- push( @{ $watches{$wd}{events} }, @new );
- }
- }
- my $watch = $watches{$wd};
- $watch->{restart} ||= $args{restart} ? 1 : 0;
- if ( $args{callback} ) {
- my $key = defined $args{key} ? $args{key} : refaddr( $args{callback} );
- # Callbacks are kept in an array so that they fire in registration order.
- my $existing = first { $_->{key} eq $key } @{ $watch->{callbacks} };
- if ($existing) {
- $existing->{code} = $args{callback};
- }
- else {
- push( @{ $watch->{callbacks} }, { key => $key, code => $args{callback} } );
- }
- }
- return $wd;
- }
- =head2 is_watched($path)
- Whether the passed path is already on the watchlist.
- =cut
- sub is_watched {
- my ($path) = @_;
- return defined $path && exists $watched{$path};
- }
- =head2 remove_watch($path)
- Drop a path (and any callbacks registered against it) from the watchlist.
- =cut
- sub remove_watch {
- my ($path) = @_;
- my $wd = delete $watched{$path};
- return 0 unless defined $wd;
- delete $watches{$wd};
- @wds = grep { $_ != $wd } @wds;
- # The watch may already be gone (deleted file), so don't blow up over it.
- eval { $inotify->remove($wd) }; ## no critic (RequireCheckingReturnValueOfEval)
- return 1;
- }
- =head2 rewatch($wd)
- Re-establish a watch which the kernel dropped out from under us.
- Anything which writes a file "atomically" (write a tempfile, rename over the
- target) destroys the watch on the old inode, so watches on individual files are
- one-shot unless we put them back. Returns the new watch descriptor, or undef
- when the path is simply gone now.
- =cut
- sub rewatch {
- my ($wd) = @_;
- my $watch = delete $watches{$wd};
- return undef unless $watch;
- delete $watched{ $watch->{path} };
- my $was_libdir = grep { $_ == $wd } @wds;
- @wds = grep { $_ != $wd } @wds;
- # Nothing to re-watch if it didn't come back.
- return undef unless -e $watch->{path};
- my $new_wd = eval {
- add_watch(
- path => $watch->{path},
- events => $watch->{events},
- restart => $watch->{restart},
- );
- };
- return undef unless defined $new_wd;
- push( @{ $watches{$new_wd}{callbacks} }, @{ $watch->{callbacks} } );
- push( @wds, $new_wd ) if $was_libdir;
- return $new_wd;
- }
- =head2 event_names($mask)
- Turn an inotify event mask into an ARRAY reference of event names.
- =cut
- sub event_names {
- my ($mask) = @_;
- return [ map { $EVENT_NAMES{$_} } grep { $mask & $_ } sort { $a <=> $b } keys(%EVENT_NAMES) ];
- }
- =head2 pending_changes()
- Drain the inotify queue and return the changes as a list of HASH references:
- {
- path => full path of the thing which changed,
- watched => the path we registered the watch against,
- events => [ 'MODIFY', ... ],
- watch => the watch entry this came from (undef if unknown to us),
- wd => inotify watch descriptor,
- mask => raw inotify mask,
- cookie => raw inotify cookie,
- name => raw inotify name (entry within a watched directory, if any),
- }
- Note that the inotify instance is shared between all the workers forked off of
- tarbaby, so whichever worker reads first consumes the event for everyone. This
- is why the callbacks you register need to be registered identically in every
- worker (declare them in your router module, which every worker loads) and need
- to be safe to run in any of them.
- =cut
- sub pending_changes {
- return () unless $inotify;
- my @changes;
- foreach my $event ( $inotify->read() ) {
- my $watch = $watches{ $event->{wd} };
- my $watched = $watch ? $watch->{path} : '';
- $watched =~ s{/+$}{};
- # Directory watches tell us the entry which changed, file watches don't.
- my $path = $watched;
- $path = $watched ne '' ? "$watched/$event->{name}" : $event->{name} if $event->{name};
- push(
- @changes,
- {
- %$event,
- path => $path,
- watched => $watched,
- events => event_names( $event->{mask} ),
- watch => $watch,
- }
- );
- }
- return @changes;
- }
- 1;
|