1package Emplacken;
2BEGIN {
3  $Emplacken::VERSION = '0.01';
4}
5
6use Moose;
7
8use Class::Load qw( is_class_loaded try_load_class );
9use Config::Any;
10use Emplacken::App;
11use Emplacken::Types qw( ArrayRef Bool Dir File );
12use Getopt::Long;
13use List::AllUtils qw( first );
14
15with 'MooseX::Getopt::Dashes';
16
17Getopt::Long::Configure('pass_through');
18
19has dir => (
20    is      => 'ro',
21    isa     => Dir,
22    coerce  => 1,
23    default => '/etc/emplacken',
24    documentation =>
25        'The directory which contains your emplacken config files',
26);
27
28has file => (
29    is            => 'ro',
30    isa           => File,
31    coerce        => 1,
32    predicate     => '_has_file',
33    documentation => 'You can supply a single file instead of a directory',
34);
35
36has verbose => (
37    is            => 'ro',
38    isa           => Bool,
39    default       => 1,
40    documentation => 'Controls whether some commands print output to stdout',
41);
42
43has __psgi_apps => (
44    traits   => ['Array'],
45    is       => 'ro',
46    isa      => ArrayRef ['Emplacken::App'],
47    init_arg => undef,
48    lazy     => 1,
49    builder  => '_build_psgi_apps',
50    handles  => {
51        _psgi_apps => 'elements',
52        _app_count => 'count',
53    },
54);
55
56sub run {
57    my $self = shift;
58
59    my $command = $self->extra_argv()->[0] || 'start';
60    my $meth = q{_} . $command;
61
62    unless ( $self->can($meth) ) {
63        die "Invalid command for emplacken: $command\n";
64    }
65
66    unless ( $self->_app_count() ) {
67        if ( $self->_has_file() ) {
68            die $self->file() . " is not a PSGI application config file\n";
69        }
70        else {
71            die "Did not find any PSGI application config files in "
72                . $self->dir() . "\n";
73        }
74    }
75
76    if ( $self->$command() ) {
77        _exit(0);
78    }
79    else {
80        _exit(1);
81    }
82}
83
84# This is a sub so we can override it for testing
85sub _exit {
86    exit shift;
87}
88
89sub _start {
90    my $self = shift;
91
92    return $self->_run_for_all_apps('start');
93}
94
95sub _stop {
96    my $self = shift;
97
98    return $self->_run_for_all_apps('start');
99}
100
101sub _restart {
102    my $self = shift;
103
104    return $self->_run_for_all_apps('stop')
105        + $self->_run_for_all_apps('start');
106}
107
108sub _run_for_all_apps {
109    my $self = shift;
110    my $meth = shift;
111
112    my $failed = 0;
113    for my $app ( $self->_psgi_apps() ) {
114
115        my $result = $app->$meth() ? 'OK' : 'failed';
116
117        my $message = sprintf(
118            "    %50s ... [%s]\n",
119            "${meth}ing " . $app->name(),
120            $result
121        );
122
123        $self->_maybe_print($message);
124    }
125
126    return !$failed;
127}
128
129sub _status {
130    my $self = shift;
131
132    for my $app ( $self->_psgi_apps() ) {
133        printf(
134            "    %50s ... [%s]\n",
135            $app->name(),
136            $app->is_running() ? 'running' : 'stopped'
137        );
138    }
139}
140
141sub _build_psgi_apps {
142    my $self = shift;
143
144    my @files
145        = $self->_has_file()
146        ? $self->file()
147        : grep { ! $_->is_dir } $self->dir()->children();
148
149    return [
150        map { $self->_build_app_from_file($_) }
151        grep {/\.conf/} grep {-s} @files
152    ];
153}
154
155sub _build_app_from_file {
156    my $self = shift;
157    my $file = shift;
158
159    my $cfg = Config::Any->load_files(
160        {
161            files           => [$file],
162            flatten_to_hash => 1,
163            use_ext         => 0,
164        }
165    );
166
167    die "$file does not seem to contain any configuration\n"
168        unless $cfg->{$file};
169
170    $cfg = $cfg->{$file};
171
172    die "$file does not contain a server key"
173        unless defined $cfg->{server} && length $cfg->{server};
174
175    my $app_class = first { try_load_class($_) } (
176        'Emplacken::App::' . $cfg->{server},
177        'Emplacken::App'
178    );
179
180    return $app_class->new( file => $file, %{$cfg} );
181}
182
183sub _maybe_print {
184    my $self = shift;
185    my $msg  = shift;
186
187    return unless $self->verbose();
188
189    print $msg;
190}
191
1921;
193
194#ABSTRACT: Manage multiple plack apps with a directory of config files
195
196
197
198=pod
199
200=head1 NAME
201
202Emplacken - Manage multiple plack apps with a directory of config files
203
204=head1 VERSION
205
206version 0.01
207
208=head1 SYNOPSIS
209
210  emplacken --dir /etc/emplacken start
211
212  emplacken --dir /etc/emplacken stop
213
214=head1 DESCRIPTION
215
216B<NOTE: This is all still experimental. Things may change in the future.>
217
218Emplacken is a tool for managing a set of L<Plack> applications based on
219config files. It also adds support for privilege dropping and error logs to
220those Plack servers that don't support these features natively.
221
222It works be reading a config file and using that to I<generate> a PSGI
223application file based on your config. It knows how to generate L<Catalyst>,
224L<Mojo>, and L<Mason> app files natively. For other apps, or more complicated
225setups, you can supply a template to Emplacken and it will use that to
226generate the PSGI app.
227
228=head1 COMMAND LINE OPTIONS
229
230The C<emplacken> command accepts either a C<--dir> or C<--file> option. If you
231don't specify either, it defaults to using the F</etc/emplacken> directory.
232
233You must also pass a command, one of C<start>, C<stop>, C<restart>, or
234C<status>.
235
236Finally, you can specify a C<--verbose> or C<--no-verbose> flag. This
237determines whether the C<start> and C<stop> command print to stdout. The
238C<status> command I<always> prints to stdout.
239
240=head1 CONFIG FILES
241
242This module uses L<Config::Any> to read config files, so you have a number of
243choices for config file syntax. These examples will use either INI or JSON
244syntax.
245
246All the config options should be in a single top-level section.
247
248=head2 Common Config Options
249
250These options are shared for all servers and code builders.
251
252For config file styles that don't support multiple values for a single option,
253you can use a comma-separated string to set multiple options.
254
255=head3 server
256
257This will be passed to the C<plackup> command to tell it what server class to
258use, for example L<Starlet> or L<Corona>. If you specify C<Starman>, then the
259C<starman> command will be used instead of C<plackup>.
260
261You can also use the value "plackup" here, which will let the Plack code pick
262the server automagically.
263
264This is required.
265
266=head3 builder
267
268The code builder to use. Currently, this can be one of L<Catalyst>, L<Mason>,
269L<Mojo>, or L<FromTemplate>. Each code builder support different config
270options. See below for details.
271
272=head3 pid_file
273
274The location of the pid file for this application.
275
276This is required.
277
278=head3 include
279
280A set of include directories to be passed to C<plackup>. You can specify
281multiple values.
282
283=head3 modules
284
285A set of modules to be passed to C<plackup>. You can specify multiple
286values. These modules will be preloaded by C<plackup>.
287
288=head3 listen
289
290This can be "HOST", "HOST:PORT", ":PORT", or a path for a Unix socket. This
291can be set multiple times, but some servers may not support multiple values.
292
293=head3 user
294
295If this is set then Emplacken will attempt to become this user before starting
296the PSGI app.
297
298=head3 group
299
300If this is set then Emplacken will attempt to become this group before
301starting the PSGI app.
302
303=head3 middleware
304
305This can be one or more middleware modules that should be enabled. Note that
306there is no way to pass config options to these modules (yet?). You can
307specify multiple values.
308
309=head3 reverse_proxy
310
311If this is true, then the L<Plack::Middleware::ReverseProxy> module is enabled
312for requests coming from 127.0.0.1.
313
314=head3 access_log
315
316If this is set to a file, then the L<Plack::Middleware::AccessLog> module is
317enabled. It will log to the specified file.
318
319=head3 access_log_format
320
321This can be used to change the access log format.
322
323=head3 error_log
324
325If this is set, then the generated PSGI app will tie C<STDERR> and log to a
326file. The log format is like Apache's error log, so you'll get something like
327this:
328
329  [Sun Dec 19 00:42:32 2010] [error] [client 1.2.3.4] Some error
330
331Any error output from Perl will be tweaked so that it fits on a single
332line. All non-printable characters will be replaced by their hex value.
333
334=head2 Starman Options
335
336If you are using the Starman server, there are several more options you can
337set in the config file.
338
339=head3 workers
340
341The number of worker processes to spawn.
342
343=head3 backlog
344
345The maximum number of backlogged listener sockets allowed.
346
347=head3 max_requests
348
349The maximum number of requests per child.
350
351=head3 preload_app
352
353If this is true, then your PSGI app is preloaded by Starman before any child
354processes are forked.
355
356=head3 disable_keepalive
357
358If this is true, then keepalive is disabled.
359
360=head2 Catalyst Options
361
362If you are using the Catalyst code builder, you must specify an C<app_class>
363config option. This is the name of the class for your web application.
364
365=head2 Mason Options
366
367If you are using the Mason code builder, you must specify C<comp_root> and
368C<data_dir> config options.
369
370=head2 Mojo Options
371
372If you are using the Mojo code builder, you must specify an C<app_class>
373config option. This is the name of the class for your web application.
374
375=head2 FromTemplate
376
377If you are using the FromTemplate code builder, you must specify a C<template>
378config option. This should be the file which contains the PSGI app template to
379use.
380
381=head2 Template Variables
382
383You can provide your own L<Text::Template> template file for Emplacken to use
384as a template when building the PSGI application file. The builder will set
385the code delimeters to C<{{> and C<}}>.
386
387You should design your template to expect several variables:
388
389=over 4
390
391=item * {{$modules}}
392
393This will be a set of C<use> statements loading any needed modules. This will
394include modules specified in the C<modules> config key, and well as additional
395modules Emplacken may require in your PSGI application file.
396
397=item * {{$pre}}
398
399This will be a chunk of code that should come before any setup code you need
400to write, and before the C<builder> block.
401
402=item * {{$builder_pre}}
403
404This should go immediately inside your C<builder> block.
405
406=item * {{$mw}}
407
408This will be a chunk of code that enables middleware. It will include
409middleware specified by the C<middleware> config option as well as anything
410else Emplacken needs (like the access log code).
411
412=item * {{$post}}
413
414This will be a chunk of code that should come after the C<builder> block at
415the end of the file.
416
417=back
418
419Here is an example template for a Catalyst application called C<MyApp>:
420
421  use strict;
422  use warnings;
423
424  use MyApp;
425  {{$modules}}
426  {{$pre}}
427
428  MyApp->setup_engine('PSGI');
429
430  builder {
431      {{ $builder_pre }}
432      {{ $mw }}
433      sub { MyApp->run(@_) };
434  };
435
436  {{$post}}
437
438=head1 DONATIONS
439
440If you'd like to thank me for the work I've done on this module, please
441consider making a "donation" to me via PayPal. I spend a lot of free time
442creating free software, and would appreciate any support you'd care to offer.
443
444Please note that B<I am not suggesting that you must do this> in order for me
445to continue working on this particular software. I will continue to do so,
446inasmuch as I have in the past, for as long as it interests me.
447
448Similarly, a donation made in this way will probably not make me work on this
449software much more, unless I get so many donations that I can consider working
450on free software full time, which seems unlikely at best.
451
452To donate, log into PayPal and send money to autarch@urth.org or use the
453button on this page: L<http://www.urth.org/~autarch/fs-donation.html>
454
455=head1 BUGS
456
457Please report any bugs or feature requests to C<bug-emplacken@rt.cpan.org>, or
458through the web interface at L<http://rt.cpan.org>.  I will be notified, and
459then you'll automatically be notified of progress on your bug as I make
460changes.
461
462=head1 AUTHOR
463
464Dave Rolsky <autarch@urth.org>
465
466=head1 COPYRIGHT AND LICENSE
467
468This software is Copyright (c) 2010 by Dave Rolsky.
469
470This is free software, licensed under:
471
472  The Artistic License 2.0
473
474=cut
475
476
477__END__
478
479