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   415245 use Mojo::Base 'Mojolicious::Plugin';
  3         22  
  3         19  
3              
4 3     3   2468 use Mojo::File 'path';
  3         50817  
  3         142  
5 3     3   17 use Mojo::Util qw(trim unquote);
  3         6  
  3         191  
6              
7 3   50 3   15 use constant DEBUG => $ENV{MOJO_SYSTEMD_DEBUG} || 0;
  3         5  
  3         4396  
8              
9             our $VERSION = '0.02';
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 1829 my ($self, $app, $config) = @_;
34             $self->_merge_config_map($config->{config_map}, $self->config_map)
35 3 50       7 if $config->{config_map};
36              
37 3   66     13 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         12 $self->_config_from_env($app->config, $self->config_map);
41             }
42              
43             sub _config_from_env {
44 8     8   930 my ($self, $config, $config_map) = @_;
45              
46 8         37 for my $k (sort keys %$config_map) {
47 60 100       129 if (ref $config_map->{$k} eq 'HASH') {
    50          
48 4   100     23 $self->_config_from_env($config->{$k} ||= {}, $config_map->{$k});
49             }
50             elsif (ref $config_map->{$k} eq 'CODE') {
51 56         84 my ($ek, $template) = $config_map->{$k}->();
52 56         66 warn sprintf "[Systemd] config %s=%s\n", $ek, $ENV{$ek} // '' if DEBUG;
53             $config->{$k} = $self->_config_val($ENV{$ek}, $template)
54 56 100       126 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   251 my ($self, $source, $target) = @_;
66              
67 2         8 for my $k (sort keys %$source) {
68 4 100       18 if (!defined $source->{$k}) {
    100          
    50          
69 1         4 delete $target->{$k};
70             }
71             elsif (ref $source->{$k} eq 'HASH') {
72 1   50     5 $self->_merge_config_map($source->{$k}, $target->{$k} ||= {});
73             }
74             elsif (ref $source->{$k} eq 'CODE') {
75 2         5 $target->{$k} = $source->{$k};
76             }
77             }
78             }
79              
80             sub _parse_environment_file {
81 3     3   507 my ($self, $file) = @_;
82 3         5 warn sprintf "[Systemd] EnvironmentFile=%s\n", $file if DEBUG;
83              
84 3 100       15 my $flag = $file =~ s!^(-)!! ? $1 : '';
85 3 100 66     91 return if $flag eq '-' and !-r $file;
86              
87 1         5 my $FH = path($file)->open;
88 1         148 while (<$FH>) {
89 7 100       118 $self->_set_environment($1, $2) if /^(\w+)=(.*)/;
90             }
91             }
92              
93             sub _parse_unit_file {
94 3     3   755 my ($self, $file) = @_;
95              
96 3         4 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         220 while (<$UNIT>) {
99 40 100       99 $self->_set_multiple_environment($1) if /^\s*\bEnvironment=(.+)/;
100 40 100       149 $self->_parse_environment_file(unquote $1) if /^\s*\bEnvironmentFile=(.+)/;
101 40 100       127 $self->_unset_multiple_environment($1) if /^\s*\bUnsetEnvironment=(.+)/;
102             }
103             }
104              
105             sub _set_environment {
106 26     26   1094 my ($self, $key, $val) = @_;
107 26         37 warn sprintf "[Systemd] set %s=%s\n", $key, unquote($val // 'undef') if DEBUG;
108 26         46 $ENV{$key} = unquote $val;
109             }
110              
111             sub _set_multiple_environment {
112 9     9   509 my ($self, $str) = @_;
113 9         22 $str =~ s!\#.*!!;
114              
115             # "FOO=word1 word2" BAR=word3 "BAZ=$word 5 6" FOO="w=1"
116 9         39 while ($str =~ m!("[^"]*"|\w+=\S+)!g) {
117 18         98 my $expr = unquote $1;
118 18 50       162 $self->_set_environment($1, $2) if $expr =~ /^(\w+)=(.*)/;
119             }
120             }
121              
122             sub _unset_multiple_environment {
123 3     3   485 my ($self, $str) = @_;
124              
125 3         20 for my $k (map { trim unquote $_ } grep {length} split /\s+/, $str) {
  8         79  
  9         17  
126 8         39 warn sprintf "[Systemd] unset %s\n", $k if DEBUG;
127 8         33 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/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