Startup.pm 10.0 KB

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