Startup.pm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. package TPSGI::Startup;
  2. # ABSTRACT: What bin/tarbaby needs before TPSGI is loaded: the configuration, and the inotify watches
  3. # Lightweight startup helpers used by bin/tarbaby before the full TPSGI.pm
  4. # is loaded into workers. Keeping these separate means updating TPSGI.pm
  5. # doesn't require restarting tarbaby to pick up get_config / inotify changes.
  6. use 5.040;
  7. use warnings FATAL => 'all';
  8. use re '/aasx';
  9. use Cwd;
  10. use File::Basename qw{dirname};
  11. use File::Find;
  12. use List::Util qw{first};
  13. use Scalar::Util qw{refaddr};
  14. use Linux::Perl::inotify;
  15. use Config::Simple;
  16. our @wds;
  17. our $inotify;
  18. # wd => { path => ..., events => [...], restart => bool, callbacks => [ { key => ..., code => ... } ] }
  19. our %watches;
  20. # path => wd, so that we don't re-issue inotify_add_watch() needlessly.
  21. our %watched;
  22. # Whether watch_for_changes() has already set up the libdir watches.
  23. our $watching = 0;
  24. our @DEFAULT_EVENTS = qw{CREATE MODIFY DELETE MOVE};
  25. # Bit => name for every event we can actually be handed back by read().
  26. my %EVENT_NAMES = map { Linux::Perl::inotify::->EVENT_NUMBER()->{$_} => $_ } qw{
  27. ACCESS MODIFY ATTRIB CLOSE_WRITE CLOSE_NOWRITE OPEN
  28. MOVED_FROM MOVED_TO CREATE DELETE DELETE_SELF MOVE_SELF
  29. UNMOUNT Q_OVERFLOW IGNORED ISDIR
  30. };
  31. =head2 get_config()
  32. Read ~/.tpsgi.ini and return a merged options hash.
  33. =cut
  34. sub get_config {
  35. $ENV{HOME} ||= Cwd::getcwd();
  36. my %options = (
  37. verbose => 0,
  38. custom_log => undef,
  39. routers => [],
  40. loggers => [],
  41. auth => undef,
  42. domain => '',
  43. user => '',
  44. binds => [],
  45. basedir => '.',
  46. http_user => '',
  47. tpsgi_dir => Cwd::getcwd(),
  48. autoreload => 0,
  49. );
  50. my $config_file = "$ENV{HOME}/.tpsgi.ini";
  51. if ( -e $config_file ) {
  52. my $conf = Config::Simple->new($config_file);
  53. my %config;
  54. %config = %{ $conf->param( -block => 'default' ) } if $conf;
  55. foreach my $opt ( keys(%options) ) {
  56. if ( ref $options{$opt} eq 'ARRAY' ) {
  57. next unless exists $config{$opt};
  58. my @arrayed = ref $config{$opt} eq 'ARRAY' ? @{ $config{$opt} } : ( $config{$opt} );
  59. push( @{ $options{$opt} }, @arrayed );
  60. next;
  61. }
  62. $options{$opt} = $config{$opt} if exists $config{$opt};
  63. }
  64. }
  65. my $LOGNAME = "$options{tpsgi_dir}/log/tpsgi.log";
  66. $LOGNAME = $options{custom_log} if $options{custom_log};
  67. my $LOGDIR = dirname($LOGNAME);
  68. $options{log_dir} = $LOGDIR;
  69. $options{log_name} = $LOGNAME;
  70. return %options;
  71. }
  72. =head2 watch_for_changes(@dirs)
  73. Set up inotify watches on the given directories (and any .pm files within them).
  74. These watches are flagged as 'restart' watches, which is what powers the
  75. autoreload feature (see TPSGI::handle_changes).
  76. Idempotent: no-op if the libdir watches are already active.
  77. =cut
  78. sub watch_for_changes {
  79. my (@dirs) = @_;
  80. return if $watching;
  81. $watching = 1;
  82. my @to_watch = @dirs;
  83. File::Find::find(
  84. {
  85. wanted => sub {
  86. my $object = $_;
  87. push( @to_watch, $object ) if ( !-d $object && $object =~ m/[.]pm$/ );
  88. },
  89. no_chdir => 1,
  90. bydepth => 1,
  91. },
  92. @dirs
  93. );
  94. foreach my $f2watch (@to_watch) {
  95. print "Watching $f2watch for changes\n";
  96. push( @wds, add_watch( path => $f2watch, restart => 1 ) );
  97. }
  98. return @wds;
  99. }
  100. =head2 add_watch(%args)
  101. Add an arbitrary file or directory to the watchlist, optionally with a callback
  102. to run when it changes. Returns the inotify watch descriptor.
  103. %args is:
  104. =over
  105. =item * C<path> - Required. File or directory to watch. When you watch a
  106. directory you will be told about changes to the entries within it (but not
  107. recursively); when you watch a file you are told about changes to that file.
  108. =item * C<callback> - Optional CODE reference, called as
  109. C<< $callback->($tpsgi, $change) >> by TPSGI::handle_changes() when the watched
  110. path changes. See that method for the shape of $change.
  111. =item * C<key> - Optional string identifying the callback. Registering the same
  112. key twice replaces the prior callback rather than stacking a second one.
  113. Defaults to the refaddr of the callback, which is what you want when you
  114. register the same named sub or package-level closure repeatedly (TPSGI::new()
  115. runs per request, so this happens constantly).
  116. =item * C<events> - Optional ARRAY reference of inotify events to watch for.
  117. Defaults to CREATE, MODIFY, DELETE and MOVE. If the path is already watched,
  118. any events not already in its mask are added to it.
  119. =item * C<restart> - Optional boolean. Marks this as a watch which should
  120. restart the server when a .pm within it changes, if autoreload is on.
  121. =back
  122. =cut
  123. sub add_watch {
  124. my (%args) = @_;
  125. my $path = $args{path};
  126. die "add_watch: path is required" if ( $path // '' ) eq '';
  127. die "add_watch: callback must be a CODE reference" if defined $args{callback} && ref $args{callback} ne 'CODE';
  128. die "add_watch: events must be an ARRAY reference" if defined $args{events} && ref $args{events} ne 'ARRAY';
  129. $inotify //= Linux::Perl::inotify::->new( flags => [qw{NONBLOCK}] );
  130. my $events = $args{events} || [@DEFAULT_EVENTS];
  131. my $wd = $watched{$path};
  132. if ( !defined $wd ) {
  133. $wd = $watched{$path} = $inotify->add( path => $path, events => $events );
  134. $watches{$wd} = {
  135. path => $path,
  136. events => [@$events],
  137. restart => 0,
  138. callbacks => [],
  139. };
  140. }
  141. elsif ( $args{events} ) {
  142. # inotify_add_watch() *replaces* the mask by default, so union it in instead.
  143. my %have = map { $_ => 1 } @{ $watches{$wd}{events} };
  144. my @new = grep { !$have{$_} } @$events;
  145. if (@new) {
  146. $inotify->add( path => $path, events => [ @new, 'MASK_ADD' ] );
  147. push( @{ $watches{$wd}{events} }, @new );
  148. }
  149. }
  150. my $watch = $watches{$wd};
  151. $watch->{restart} ||= $args{restart} ? 1 : 0;
  152. if ( $args{callback} ) {
  153. my $key = defined $args{key} ? $args{key} : refaddr( $args{callback} );
  154. # Callbacks are kept in an array so that they fire in registration order.
  155. my $existing = first { $_->{key} eq $key } @{ $watch->{callbacks} };
  156. if ($existing) {
  157. $existing->{code} = $args{callback};
  158. }
  159. else {
  160. push( @{ $watch->{callbacks} }, { key => $key, code => $args{callback} } );
  161. }
  162. }
  163. return $wd;
  164. }
  165. =head2 is_watched($path)
  166. Whether the passed path is already on the watchlist.
  167. =cut
  168. sub is_watched {
  169. my ($path) = @_;
  170. return defined $path && exists $watched{$path};
  171. }
  172. =head2 remove_watch($path)
  173. Drop a path (and any callbacks registered against it) from the watchlist.
  174. =cut
  175. sub remove_watch {
  176. my ($path) = @_;
  177. my $wd = delete $watched{$path};
  178. return 0 unless defined $wd;
  179. delete $watches{$wd};
  180. @wds = grep { $_ != $wd } @wds;
  181. # The watch may already be gone (deleted file), so don't blow up over it.
  182. eval { $inotify->remove($wd) }; ## no critic (RequireCheckingReturnValueOfEval)
  183. return 1;
  184. }
  185. =head2 rewatch($wd)
  186. Re-establish a watch which the kernel dropped out from under us.
  187. Anything which writes a file "atomically" (write a tempfile, rename over the
  188. target) destroys the watch on the old inode, so watches on individual files are
  189. one-shot unless we put them back. Returns the new watch descriptor, or undef
  190. when the path is simply gone now.
  191. =cut
  192. sub rewatch {
  193. my ($wd) = @_;
  194. my $watch = delete $watches{$wd};
  195. return undef unless $watch;
  196. delete $watched{ $watch->{path} };
  197. my $was_libdir = grep { $_ == $wd } @wds;
  198. @wds = grep { $_ != $wd } @wds;
  199. # Nothing to re-watch if it didn't come back.
  200. return undef unless -e $watch->{path};
  201. my $new_wd = eval {
  202. add_watch(
  203. path => $watch->{path},
  204. events => $watch->{events},
  205. restart => $watch->{restart},
  206. );
  207. };
  208. return undef unless defined $new_wd;
  209. push( @{ $watches{$new_wd}{callbacks} }, @{ $watch->{callbacks} } );
  210. push( @wds, $new_wd ) if $was_libdir;
  211. return $new_wd;
  212. }
  213. =head2 event_names($mask)
  214. Turn an inotify event mask into an ARRAY reference of event names.
  215. =cut
  216. sub event_names {
  217. my ($mask) = @_;
  218. return [ map { $EVENT_NAMES{$_} } grep { $mask & $_ } sort { $a <=> $b } keys(%EVENT_NAMES) ];
  219. }
  220. =head2 pending_changes()
  221. Drain the inotify queue and return the changes as a list of HASH references:
  222. {
  223. path => full path of the thing which changed,
  224. watched => the path we registered the watch against,
  225. events => [ 'MODIFY', ... ],
  226. watch => the watch entry this came from (undef if unknown to us),
  227. wd => inotify watch descriptor,
  228. mask => raw inotify mask,
  229. cookie => raw inotify cookie,
  230. name => raw inotify name (entry within a watched directory, if any),
  231. }
  232. Note that the inotify instance is shared between all the workers forked off of
  233. tarbaby, so whichever worker reads first consumes the event for everyone. This
  234. is why the callbacks you register need to be registered identically in every
  235. worker (declare them in your router module, which every worker loads) and need
  236. to be safe to run in any of them.
  237. =cut
  238. sub pending_changes {
  239. return () unless $inotify;
  240. my @changes;
  241. foreach my $event ( $inotify->read() ) {
  242. my $watch = $watches{ $event->{wd} };
  243. my $watched = $watch ? $watch->{path} : '';
  244. $watched =~ s{/+$}{};
  245. # Directory watches tell us the entry which changed, file watches don't.
  246. my $path = $watched;
  247. $path = $watched ne '' ? "$watched/$event->{name}" : $event->{name} if $event->{name};
  248. push(
  249. @changes,
  250. {
  251. %$event,
  252. path => $path,
  253. watched => $watched,
  254. events => event_names( $event->{mask} ),
  255. watch => $watch,
  256. }
  257. );
  258. }
  259. return @changes;
  260. }
  261. 1;