File Coverage

blib/lib/Mojolicious/Plugin/Systemd.pm
Criterion Covered Total %
statement 60 60 100.0
branch 28 32 87.5
condition 13 17 76.4
subroutine 13 13 100.0
pod 1 1 100.0
total 115 123 93.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Systemd;
2 3     3   417573 use Mojo::Base 'Mojolicious::Plugin';
  3         21  
  3         21  
3              
4 3     3   2454 use Mojo::File 'path';
  3         50753  
  3         151  
5 3     3   19 use Mojo::Util qw(trim unquote);
  3         7  
  3         194  
6              
7 3   50 3   17 use constant DEBUG => $ENV{MOJO_SYSTEMD_DEBUG} || 0;
  3         5  
  3         4374  
8              
9             our $VERSION = '0.01';
10              
11             has config_map => sub {
12             return {
13             hypnotoad => {
14             accepts => sub { (MOJO_SERVER_ACCEPTS => 0) },
15             backlog => sub { (MOJO_SERVER_BACKLOG => 0) },
16             clients => sub { (MOJO_SERVER_CLIENTS => 0) },
17             graceful_timeout => sub { (MOJO_SERVER_GRACEFUL_TIMEOUT => 0) },
18             heartbeat_interval => sub { (MOJO_SERVER_HEARTBEAT_INTERVAL => 0) },
19             heartbeat_timeout => sub { (MOJO_SERVER_HEARTBEAT_TIMEOUT => 0) },
20             inactivity_timeout => sub { (MOJO_SERVER_INACTIVITY_TIMEOUT => 0) },
21             listen => sub { (MOJO_LISTEN => [qr{\s+}]) },
22             pid_file => sub { (MOJO_SERVER_PID_FILE => '') },
23             proxy => sub { (MOJO_SERVER_PROXY => 0) },
24             requests => sub { (MOJO_SERVER_REQUESTS => 0) },
25             spare => sub { (MOJO_SERVER_SPARE => 0) },
26             upgrade_timeout => sub { (MOJO_SERVER_UPGRADE_TIMEOUT => 0) },
27             workers => sub { (MOJO_SERVER_WORKERS => 0) },
28             },
29             };
30             };
31              
32             sub register {
33 3     3 1 1741 my ($self, $app, $config) = @_;
34             $self->_merge_config_map($config->{config_map}, $self->config_map)
35 3 50       9 if $config->{config_map};
36              
37 3   66     12 my $file = $config->{service_file} || $ENV{SYSTEMD_SERVICE_FILE};
38 3 100 100     12 $self->_parse_unit_file($file) if $file or $ENV{XDG_SESSION_ID};
39              
40 2         10 $self->_config_from_env($app->config, $self->config_map);
41             }
42              
43             sub _config_from_env {
44 8     8   1135 my ($self, $config, $config_map) = @_;
45              
46 8         37 for my $k (sort keys %$config_map) {
47 60 100       126 if (ref $config_map->{$k} eq 'HASH') {
    50          
48 4   100     25 $self->_config_from_env($config->{$k} ||= {}, $config_map->{$k});
49             }
50             elsif (ref $config_map->{$k} eq 'CODE') {
51 56         83 my ($ek, $template) = $config_map->{$k}->();
52 56         77 warn sprintf "[Systemd] config %s=%s\n", $ek, $ENV{$ek} // '' if DEBUG;
53             $config->{$k} = $self->_config_val($ENV{$ek}, $template)
54 56 100       138 if defined $ENV{$ek};
55             }
56             }
57             }
58              
59             sub _config_val {
60 5     5   11 my ($self, $val, $template) = @_;
61 5 100       30 return ref $template eq 'ARRAY' ? [split $template->[0], $val] : $val;
62             }
63              
64             sub _merge_config_map {
65 2     2   259 my ($self, $source, $target) = @_;
66              
67 2         7 for my $k (sort keys %$source) {
68 4 100       17 if (!defined $source->{$k}) {
    100          
    50          
69 1         4 delete $target->{$k};
70             }
71             elsif (ref $source->{$k} eq 'HASH') {
72 1   50     6 $self->_merge_config_map($source->{$k}, $target->{$k} ||= {});
73             }
74             elsif (ref $source->{$k} eq 'CODE') {
75 2         6 $target->{$k} = $source->{$k};
76             }
77             }
78             }
79              
80             sub _parse_environment_file {
81 3     3   396 my ($self, $file) = @_;
82 3         4 warn sprintf "[Systemd] EnvironmentFile=%s\n", $file if DEBUG;
83              
84 3 100       13 my $flag = $file =~ s!^(-)!! ? $1 : '';
85 3 100 66     86 return if $flag eq '-' and !-r $file;
86              
87 1         3 my $FH = path($file)->open;
88 1         117 while (<$FH>) {
89 7 100       93 $self->_set_environment($1, $2) if /^(\w+)=(.*)/;
90             }
91             }
92              
93             sub _parse_unit_file {
94 3     3   689 my ($self, $file) = @_;
95              
96 3         6 warn sprintf "[Systemd] SYSTEMD_UNIT_FILE=%s\n", $file if DEBUG;
97 3   100     14 my $UNIT = path($file || 'SYSTEMD_UNIT_FILE_MISSING')->open;
98 2         200 while (<$UNIT>) {
99 40 100       102 $self->_set_multiple_environment($1) if /^\s*\bEnvironment=(.+)/;
100 40 100       126 $self->_parse_environment_file(unquote $1) if /^\s*\bEnvironmentFile=(.+)/;
101 40 100       131 $self->_unset_multiple_environment($1) if /^\s*\bUnsetEnvironment=(.+)/;
102             }
103             }
104              
105             sub _set_environment {
106 26     26   1231 my ($self, $key, $val) = @_;
107 26         32 warn sprintf "[Systemd] set %s=%s\n", $key, unquote($val // 'undef') if DEBUG;
108 26         66 $ENV{$key} = unquote $val;
109             }
110              
111             sub _set_multiple_environment {
112 9     9   527 my ($self, $str) = @_;
113 9         23 $str =~ s!\#.*!!;
114              
115             # "FOO=word1 word2" BAR=word3 "BAZ=$word 5 6" FOO="w=1"
116 9         35 while ($str =~ m!("[^"]*"|\w+=\S+)!g) {
117 18         99 my $expr = unquote $1;
118 18 50       158 $self->_set_environment($1, $2) if $expr =~ /^(\w+)=(.*)/;
119             }
120             }
121              
122             sub _unset_multiple_environment {
123 3     3   507 my ($self, $str) = @_;
124              
125 3         18 for my $k (map { trim unquote $_ } grep {length} split /\s+/, $str) {
  8         114  
  9         17  
126 8         44 warn sprintf "[Systemd] unset %s\n", $k if DEBUG;
127 8         30 delete $ENV{$k};
128             }
129             }
130              
131             1;
132              
133             =encoding utf8
134              
135             =head1 NAME
136              
137             Mojolicious::Plugin::Systemd - Configure your app from within systemd service file
138              
139             =head1 SYNOPSIS
140              
141             =head2 Example application
142              
143             package MyApp;
144             use Mojo::Base "Mojolicious";
145             sub startup {
146             my $app = shift;
147             $app->plugin("Systemd");
148             }
149              
150             =head2 Example systemd unit file
151              
152             [Unit]
153             Description=MyApp service
154             After=network.target
155              
156             [Service]
157             Environment=SYSTEMD_SERVICE_FILE=/etc/systemd/system/my_app.service
158             Environment=MOJO_SERVER_PID_FILE=/var/run/my_app.pid
159             Environment=MYAPP_HOME=/var/my_app
160             EnvironmentFile=-/etc/default/my_app
161              
162             User=www
163             Type=forking
164             PIDFile=/var/run/my_app.pid
165             ExecStart=/path/to/hypnotoad /home/myapp/script/my_app
166             ExecReload=/path/to/hypnotoad /home/myapp/script/my_app
167             KillMode=process
168             SyslogIdentifier=my_app
169              
170             [Install]
171             WantedBy=multi-user.target
172              
173             =head1 DESCRIPTION
174              
175             L is a L plugin that allows your
176             application to read configuration from a Systemd service (unit) file.
177              
178             It works by parsing the C, C and
179             C statements in the service file and inject those environment
180             variables into your application. This is especially useful if your application
181             is run by L, since you cannot "inject" environment
182             variables into a running application, meaning C below won't change
183             anything in your already started application:
184              
185             $ SOME_VAR=42 /path/to/hypnotoad /home/sri/myapp/script/my_app
186              
187             See L
188             for more information about C, C and C.
189              
190             =head1 ATTRIBUTES
191              
192             =head2 config_map
193              
194             $hash_ref = $self->config_map;
195              
196             Returns a structure for how L can be set from environment
197             variables. By default the environment variables below are supported:
198              
199             $app->config->{hypnotoad}{accepts} = $ENV{MOJO_SERVER_ACCEPTS}
200             $app->config->{hypnotoad}{backlog} = $ENV{MOJO_SERVER_BACKLOG}
201             $app->config->{hypnotoad}{clients} = $ENV{MOJO_SERVER_CLIENTS}
202             $app->config->{hypnotoad}{graceful_timeout} = $ENV{MOJO_SERVER_GRACEFUL_TIMEOUT}
203             $app->config->{hypnotoad}{heartbeat_interval} = $ENV{MOJO_SERVER_HEARTBEAT_INTERVAL}
204             $app->config->{hypnotoad}{heartbeat_timeout} = $ENV{MOJO_SERVER_HEARTBEAT_TIMEOUT}
205             $app->config->{hypnotoad}{inactivity_timeout} = $ENV{MOJO_SERVER_INACTIVITY_TIMEOUT}
206             $app->config->{hypnotoad}{listen} = [split /\s+/, $ENV{MOJO_LISTEN}];
207             $app->config->{hypnotoad}{pid_file} = $ENV{MOJO_SERVER_PID_FILE}
208             $app->config->{hypnotoad}{proxy} = $ENV{MOJO_SERVER_PROXY}
209             $app->config->{hypnotoad}{requests} = $ENV{MOJO_SERVER_REQUESTS}
210             $app->config->{hypnotoad}{spare} = $ENV{MOJO_SERVER_SPARE}
211             $app->config->{hypnotoad}{upgrade_timeout} = $ENV{MOJO_SERVER_UPGRADE_TIMEOUT}
212             $app->config->{hypnotoad}{workers} = $ENV{MOJO_SERVER_WORKERS}
213              
214             =head1 METHODS
215              
216             =head2 register
217              
218             $app->plugin("Systemd");
219             $app->plugin("Systemd" => {config_map => {...}, service_file => "..."});
220              
221             Used to register the plugin in your application. The following options are
222             otional:
223              
224             =over 2
225              
226             =item * config_map
227              
228             The C must be a hash-ref and will be I with the
229             L attribute. Example:
230              
231             $app->plugin(Systemd => {
232             config_map => {
233             # Add your own custom environment variables. The empty quotes means
234             # that the environment variable should be read as a string.
235             database => {
236             url => sub { (MYAPP_DB_URL => "") },
237             },
238             hypnotoad => {
239             # Remove support for the default MOJO_SERVER_ACCEPTS environment
240             # variable
241             accepts => undef,
242              
243             # Change the environment variable from MOJO_LISTEN and
244             # the regexp to split the environment variable into a list
245             listen => sub { (MYAPP_LISTEN => [qr{[,\s]}]) },
246             }
247             }
248             });
249              
250             =item * service_file
251              
252             Defaults to the environment variable C and I required
253             if C is set. Must be a full path to where your service file is
254             located. See L for example.
255              
256             =back
257              
258             =head1 AUTHOR
259              
260             Jan Henning Thorsen
261              
262             =head1 COPYRIGHT AND LICENSE
263              
264             Copyright (C) 2019, Jan Henning Thorsen.
265              
266             This program is free software, you can redistribute it and/or modify it under
267             the terms of the Artistic License version 2.0.
268              
269             =head1 SEE ALSO
270              
271             L, L.
272              
273             =cut