wheneverd 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile.lock +38 -33
- data/README.md +19 -5
- data/lib/wheneverd/cli/activate.rb +5 -5
- data/lib/wheneverd/cli/deactivate.rb +6 -6
- data/lib/wheneverd/cli/reload.rb +8 -8
- data/lib/wheneverd/cli/status.rb +13 -6
- data/lib/wheneverd/cli.rb +10 -8
- data/lib/wheneverd/dsl/context.rb +46 -0
- data/lib/wheneverd/schedule.rb +15 -1
- data/lib/wheneverd/service.rb +105 -0
- data/lib/wheneverd/systemd/renderer.rb +27 -4
- data/lib/wheneverd/systemd/unit_content_builder.rb +23 -0
- data/lib/wheneverd/systemd/unit_namer.rb +0 -1
- data/lib/wheneverd/version.rb +1 -1
- data/lib/wheneverd.rb +1 -0
- data/test/cli_activate_test.rb +27 -0
- data/test/cli_reload_test.rb +23 -0
- data/test/cli_status_test.rb +14 -4
- data/test/dsl_context_shell_test.rb +31 -0
- data/test/systemd_renderer_test.rb +73 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9469e813aa6390a8e7eb911ce672a9fdaf7ab861e435d71d2d8887d551f8b8f8
|
|
4
|
+
data.tar.gz: dcf0211fe96fe0faff489a82ab3d0f12f36c3703083a949606eed4ab8eb9fa28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a448263264d7322c6622d111f5bf2f51f916ee0dc22193388e4b8b15a4336c3d136c475102db7c772b5be35fd53fa3800fcb733ef3850d620e982baeb9fa9a9
|
|
7
|
+
data.tar.gz: 62aad81a000a60510e6b230660321ec40951ecdcaed1317517f33fa10688fa319aa2afc43f8b003704b364d0f0f68a1d12e95482dce408208269a400ebee9a87
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.3
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ On release, entries are moved into `## x.y.z` sections that match the gem versio
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.5.0
|
|
9
|
+
|
|
10
|
+
- Adds top-level `service` DSL entries for long-running systemd user services.
|
|
11
|
+
- `activate`, `deactivate`, `reload`, and `status` now manage standalone services alongside timers.
|
|
12
|
+
- Standalone services render as `Type=simple` units with configurable `Restart=`, `RestartSec=`, and extra `[Service]` settings.
|
|
13
|
+
|
|
8
14
|
## 0.4.0
|
|
9
15
|
|
|
10
16
|
- Docs: adds a copy/paste "deploy a simple schedule" example and refines README status section.
|
data/Gemfile.lock
CHANGED
|
@@ -1,61 +1,64 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
wheneverd (0.
|
|
4
|
+
wheneverd (0.5.0)
|
|
5
5
|
clamp (~> 1.3)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
10
|
ast (2.4.3)
|
|
11
|
-
clamp (1.
|
|
11
|
+
clamp (1.5.2)
|
|
12
12
|
date (3.5.1)
|
|
13
13
|
docile (1.4.1)
|
|
14
|
-
|
|
14
|
+
drb (2.2.3)
|
|
15
|
+
erb (6.0.4)
|
|
15
16
|
io-console (0.8.2)
|
|
16
|
-
irb (1.
|
|
17
|
+
irb (1.18.0)
|
|
17
18
|
pp (>= 0.6.0)
|
|
19
|
+
prism (>= 1.3.0)
|
|
18
20
|
rdoc (>= 4.0.0)
|
|
19
21
|
reline (>= 0.4.2)
|
|
20
|
-
json (2.
|
|
22
|
+
json (2.19.4)
|
|
21
23
|
language_server-protocol (3.17.0.5)
|
|
22
24
|
lint_roller (1.1.0)
|
|
23
|
-
minitest (6.0.
|
|
25
|
+
minitest (6.0.5)
|
|
26
|
+
drb (~> 2.0)
|
|
24
27
|
prism (~> 1.5)
|
|
25
|
-
parallel (1.
|
|
26
|
-
parser (3.3.
|
|
28
|
+
parallel (2.1.0)
|
|
29
|
+
parser (3.3.11.1)
|
|
27
30
|
ast (~> 2.4.1)
|
|
28
31
|
racc
|
|
29
32
|
pp (0.6.3)
|
|
30
33
|
prettyprint
|
|
31
34
|
prettyprint (0.2.0)
|
|
32
|
-
prism (1.
|
|
35
|
+
prism (1.9.0)
|
|
33
36
|
psych (5.3.1)
|
|
34
37
|
date
|
|
35
38
|
stringio
|
|
36
39
|
racc (1.8.1)
|
|
37
40
|
rainbow (3.1.1)
|
|
38
|
-
rake (13.
|
|
39
|
-
rdoc (7.
|
|
41
|
+
rake (13.4.2)
|
|
42
|
+
rdoc (7.2.0)
|
|
40
43
|
erb
|
|
41
44
|
psych (>= 4.0.0)
|
|
42
45
|
tsort
|
|
43
46
|
redcarpet (3.6.1)
|
|
44
|
-
regexp_parser (2.
|
|
47
|
+
regexp_parser (2.12.0)
|
|
45
48
|
reline (0.6.3)
|
|
46
49
|
io-console (~> 0.5)
|
|
47
|
-
rubocop (1.
|
|
50
|
+
rubocop (1.86.1)
|
|
48
51
|
json (~> 2.3)
|
|
49
52
|
language_server-protocol (~> 3.17.0.2)
|
|
50
53
|
lint_roller (~> 1.1.0)
|
|
51
|
-
parallel (
|
|
54
|
+
parallel (>= 1.10)
|
|
52
55
|
parser (>= 3.3.0.2)
|
|
53
56
|
rainbow (>= 2.2.2, < 4.0)
|
|
54
57
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
55
|
-
rubocop-ast (>= 1.
|
|
58
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
56
59
|
ruby-progressbar (~> 1.7)
|
|
57
60
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
58
|
-
rubocop-ast (1.49.
|
|
61
|
+
rubocop-ast (1.49.1)
|
|
59
62
|
parser (>= 3.3.7.2)
|
|
60
63
|
prism (~> 1.7)
|
|
61
64
|
ruby-progressbar (1.13.0)
|
|
@@ -70,7 +73,7 @@ GEM
|
|
|
70
73
|
unicode-display_width (3.2.0)
|
|
71
74
|
unicode-emoji (~> 4.1)
|
|
72
75
|
unicode-emoji (4.2.0)
|
|
73
|
-
yard (0.9.
|
|
76
|
+
yard (0.9.43)
|
|
74
77
|
|
|
75
78
|
PLATFORMS
|
|
76
79
|
ruby
|
|
@@ -89,31 +92,33 @@ DEPENDENCIES
|
|
|
89
92
|
|
|
90
93
|
CHECKSUMS
|
|
91
94
|
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
|
|
92
|
-
|
|
95
|
+
bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
|
|
96
|
+
clamp (1.5.2) sha256=2fed212e5c9b60447eb5097af39d4c12c3c7e6788e8d791e9c436e0e755f4adc
|
|
93
97
|
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
|
|
94
98
|
docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
|
|
95
|
-
|
|
99
|
+
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
|
|
100
|
+
erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
|
|
96
101
|
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
|
|
97
|
-
irb (1.
|
|
98
|
-
json (2.
|
|
102
|
+
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
|
|
103
|
+
json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
|
|
99
104
|
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
|
|
100
105
|
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
|
|
101
|
-
minitest (6.0.
|
|
102
|
-
parallel (1.
|
|
103
|
-
parser (3.3.
|
|
106
|
+
minitest (6.0.5) sha256=f007d7246bf4feea549502842cd7c6aba8851cdc9c90ba06de9c476c0d01155c
|
|
107
|
+
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
|
|
108
|
+
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
|
|
104
109
|
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
|
|
105
110
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
106
|
-
prism (1.
|
|
111
|
+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
|
107
112
|
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
|
|
108
113
|
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
|
|
109
114
|
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
|
|
110
|
-
rake (13.
|
|
111
|
-
rdoc (7.
|
|
115
|
+
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
|
116
|
+
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
|
|
112
117
|
redcarpet (3.6.1) sha256=d444910e6aa55480c6bcdc0cdb057626e8a32c054c29e793fa642ba2f155f445
|
|
113
|
-
regexp_parser (2.
|
|
118
|
+
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
|
114
119
|
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
|
|
115
|
-
rubocop (1.
|
|
116
|
-
rubocop-ast (1.49.
|
|
120
|
+
rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
|
|
121
|
+
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
|
|
117
122
|
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
|
|
118
123
|
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
|
|
119
124
|
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
|
|
@@ -122,8 +127,8 @@ CHECKSUMS
|
|
|
122
127
|
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
|
|
123
128
|
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
|
|
124
129
|
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
|
|
125
|
-
wheneverd (0.
|
|
126
|
-
yard (0.9.
|
|
130
|
+
wheneverd (0.5.0)
|
|
131
|
+
yard (0.9.43) sha256=cf8733a8f0485df2a162927e9b5f182215a61f6d22de096b8f402c726a1c5821
|
|
127
132
|
|
|
128
133
|
BUNDLED WITH
|
|
129
|
-
4.0.
|
|
134
|
+
4.0.11
|
data/README.md
CHANGED
|
@@ -167,7 +167,7 @@ Schedules are defined in a Ruby file (default: `config/schedule.rb`) and evaluat
|
|
|
167
167
|
|
|
168
168
|
Note: schedule files are executed as Ruby. Do not run untrusted schedule code.
|
|
169
169
|
|
|
170
|
-
The core shape is:
|
|
170
|
+
The core timer shape is:
|
|
171
171
|
|
|
172
172
|
```ruby
|
|
173
173
|
every(period, at: nil) do
|
|
@@ -175,6 +175,20 @@ every(period, at: nil) do
|
|
|
175
175
|
end
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
Long-running services can be managed from the same schedule with top-level
|
|
179
|
+
`service` entries:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
service "worker",
|
|
183
|
+
shell: "bundle exec bin/worker",
|
|
184
|
+
restart: "always",
|
|
185
|
+
restart_sec: "5s",
|
|
186
|
+
service: {
|
|
187
|
+
"WorkingDirectory" => "/srv/apps/myapp/current",
|
|
188
|
+
"Environment" => "RAILS_ENV=production"
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
178
192
|
For calendar schedules, you can also pass multiple period symbols (or an array) to run the same jobs on multiple days:
|
|
179
193
|
|
|
180
194
|
```ruby
|
|
@@ -270,14 +284,14 @@ Commands:
|
|
|
270
284
|
|
|
271
285
|
- `wheneverd init [--schedule PATH] [--force]` writes a template schedule file.
|
|
272
286
|
- `wheneverd show [--schedule PATH] [--identifier NAME]` prints rendered units to stdout.
|
|
273
|
-
- `wheneverd status [--identifier NAME] [--unit-dir PATH]` prints `systemctl --user list-timers` and `systemctl --user status` for installed timers.
|
|
287
|
+
- `wheneverd status [--identifier NAME] [--unit-dir PATH]` prints `systemctl --user list-timers` and `systemctl --user status` for installed timers/services.
|
|
274
288
|
- `wheneverd diff [--schedule PATH] [--identifier NAME] [--unit-dir PATH]` diffs rendered units vs unit files on disk.
|
|
275
289
|
- `wheneverd validate [--schedule PATH] [--identifier NAME] [--verify]` validates rendered `OnCalendar=` values via `systemd-analyze calendar` (and with `--verify`, runs `systemd-analyze --user verify` on temporary unit files).
|
|
276
290
|
- `wheneverd write [--schedule PATH] [--identifier NAME] [--unit-dir PATH] [--dry-run] [--[no-]prune]` writes units to disk (or prints paths in `--dry-run` mode).
|
|
277
291
|
- `wheneverd delete [--identifier NAME] [--unit-dir PATH] [--dry-run]` deletes previously generated units for the identifier.
|
|
278
|
-
- `wheneverd activate [--schedule PATH] [--identifier NAME]` runs `systemctl --user daemon-reload` and enables/starts the timers.
|
|
279
|
-
- `wheneverd deactivate [--schedule PATH] [--identifier NAME]` stops and disables the timers.
|
|
280
|
-
- `wheneverd reload [--schedule PATH] [--identifier NAME] [--unit-dir PATH] [--[no-]prune]` writes units, reloads systemd, and restarts timers.
|
|
292
|
+
- `wheneverd activate [--schedule PATH] [--identifier NAME]` runs `systemctl --user daemon-reload` and enables/starts the timers/services.
|
|
293
|
+
- `wheneverd deactivate [--schedule PATH] [--identifier NAME]` stops and disables the timers/services.
|
|
294
|
+
- `wheneverd reload [--schedule PATH] [--identifier NAME] [--unit-dir PATH] [--[no-]prune]` writes units, reloads systemd, and restarts timers/services.
|
|
281
295
|
- `wheneverd current [--identifier NAME] [--unit-dir PATH]` prints the currently installed unit file contents from disk.
|
|
282
296
|
- `wheneverd linger [--user NAME] [enable|disable|status]` manages lingering via `loginctl` (`status` is the default).
|
|
283
297
|
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Wheneverd
|
|
4
|
-
# Implements `wheneverd activate` (enable + start timers via `systemctl --user`).
|
|
4
|
+
# Implements `wheneverd activate` (enable + start timers/services via `systemctl --user`).
|
|
5
5
|
class CLI::Activate < CLI
|
|
6
6
|
def execute
|
|
7
|
-
|
|
8
|
-
return 0 if
|
|
7
|
+
units = activatable_unit_basenames
|
|
8
|
+
return 0 if units.empty?
|
|
9
9
|
|
|
10
10
|
Wheneverd::Systemd::Systemctl.run("daemon-reload")
|
|
11
|
-
Wheneverd::Systemd::Systemctl.run("enable", "--now", *
|
|
11
|
+
Wheneverd::Systemd::Systemctl.run("enable", "--now", *units)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
units.each { |unit| puts unit }
|
|
14
14
|
0
|
|
15
15
|
rescue StandardError => e
|
|
16
16
|
handle_error(e)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Wheneverd
|
|
4
|
-
# Implements `wheneverd deactivate` (stop + disable timers via `systemctl --user`).
|
|
4
|
+
# Implements `wheneverd deactivate` (stop + disable timers/services via `systemctl --user`).
|
|
5
5
|
class CLI::Deactivate < CLI
|
|
6
6
|
def execute
|
|
7
|
-
|
|
8
|
-
return 0 if
|
|
7
|
+
units = activatable_unit_basenames
|
|
8
|
+
return 0 if units.empty?
|
|
9
9
|
|
|
10
|
-
Wheneverd::Systemd::Systemctl.run("stop", *
|
|
11
|
-
Wheneverd::Systemd::Systemctl.run("disable", *
|
|
10
|
+
Wheneverd::Systemd::Systemctl.run("stop", *units)
|
|
11
|
+
Wheneverd::Systemd::Systemctl.run("disable", *units)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
units.each { |unit| puts unit }
|
|
14
14
|
0
|
|
15
15
|
rescue StandardError => e
|
|
16
16
|
handle_error(e)
|
data/lib/wheneverd/cli/reload.rb
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Wheneverd
|
|
4
|
-
# Implements `wheneverd reload` (write units, reload daemon, restart timers).
|
|
4
|
+
# Implements `wheneverd reload` (write units, reload daemon, restart timers/services).
|
|
5
5
|
class CLI::Reload < CLI
|
|
6
6
|
option "--[no-]prune", :flag,
|
|
7
7
|
"Prune previously generated units for the identifier (default: enabled)",
|
|
8
8
|
default: true
|
|
9
9
|
|
|
10
10
|
def execute
|
|
11
|
-
paths,
|
|
12
|
-
return 0 if
|
|
11
|
+
paths, units = write_units_and_activatable_basenames
|
|
12
|
+
return 0 if units.empty?
|
|
13
13
|
|
|
14
|
-
reload_systemd(
|
|
14
|
+
reload_systemd(units)
|
|
15
15
|
|
|
16
16
|
paths.each { |path| puts path }
|
|
17
17
|
0
|
|
@@ -21,7 +21,7 @@ module Wheneverd
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def write_units_and_activatable_basenames
|
|
25
25
|
units = render_units
|
|
26
26
|
paths = Wheneverd::Systemd::UnitWriter.write(
|
|
27
27
|
units,
|
|
@@ -29,12 +29,12 @@ module Wheneverd
|
|
|
29
29
|
prune: prune?,
|
|
30
30
|
identifier: identifier_value
|
|
31
31
|
)
|
|
32
|
-
[paths,
|
|
32
|
+
[paths, activatable_unit_basenames(units)]
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def reload_systemd(
|
|
35
|
+
def reload_systemd(units)
|
|
36
36
|
Wheneverd::Systemd::Systemctl.run("daemon-reload")
|
|
37
|
-
Wheneverd::Systemd::Systemctl.run("restart", *
|
|
37
|
+
Wheneverd::Systemd::Systemctl.run("restart", *units)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
data/lib/wheneverd/cli/status.rb
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Wheneverd
|
|
4
|
-
# Implements `wheneverd status` (show installed timer status via `systemctl --user`).
|
|
4
|
+
# Implements `wheneverd status` (show installed timer/service status via `systemctl --user`).
|
|
5
5
|
class CLI::Status < CLI
|
|
6
6
|
def execute
|
|
7
|
-
timer_units =
|
|
8
|
-
|
|
7
|
+
timer_units, service_units = installed_unit_basenames
|
|
8
|
+
units = timer_units + service_units
|
|
9
|
+
return 0 if units.empty?
|
|
9
10
|
|
|
10
11
|
print_list_timers(timer_units)
|
|
11
|
-
print_status(
|
|
12
|
+
print_status(units)
|
|
12
13
|
0
|
|
13
14
|
rescue StandardError => e
|
|
14
15
|
handle_error(e)
|
|
@@ -17,12 +18,18 @@ module Wheneverd
|
|
|
17
18
|
private
|
|
18
19
|
|
|
19
20
|
# @return [Array<String>]
|
|
20
|
-
def
|
|
21
|
+
def installed_unit_basenames
|
|
21
22
|
paths = Wheneverd::Systemd::UnitLister.list(identifier: identifier_value, unit_dir: unit_dir)
|
|
22
|
-
paths.map { |path| File.basename(path) }
|
|
23
|
+
basenames = paths.map { |path| File.basename(path) }
|
|
24
|
+
timers = basenames.grep(/\.timer\z/).uniq
|
|
25
|
+
timer_managed_services = timers.map { |timer| timer.sub(/\.timer\z/, ".service") }
|
|
26
|
+
services = basenames.grep(/\.service\z/).uniq - timer_managed_services
|
|
27
|
+
[timers, services]
|
|
23
28
|
end
|
|
24
29
|
|
|
25
30
|
def print_list_timers(timer_units)
|
|
31
|
+
return if timer_units.empty?
|
|
32
|
+
|
|
26
33
|
stdout, = Wheneverd::Systemd::Systemctl.run("list-timers", "--all", *timer_units)
|
|
27
34
|
print stdout
|
|
28
35
|
end
|
data/lib/wheneverd/cli.rb
CHANGED
|
@@ -51,12 +51,14 @@ module Wheneverd
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# @param units [Array<Wheneverd::Systemd::Unit>]
|
|
54
|
-
# @return [Array<String>] timer unit basenames
|
|
55
|
-
def
|
|
56
|
-
units.select { |unit|
|
|
54
|
+
# @return [Array<String>] timer and standalone service unit basenames
|
|
55
|
+
def activatable_unit_basenames(units = render_units)
|
|
56
|
+
units.select { |unit| %i[timer service].include?(unit.activation) }
|
|
57
|
+
.map(&:path_basename)
|
|
58
|
+
.uniq
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
private :render_units, :
|
|
61
|
+
private :render_units, :activatable_unit_basenames
|
|
60
62
|
end
|
|
61
63
|
end
|
|
62
64
|
|
|
@@ -81,14 +83,14 @@ module Wheneverd
|
|
|
81
83
|
subcommand "help", "Show help", Wheneverd::CLI::Help
|
|
82
84
|
subcommand "init", "Create a schedule template", Wheneverd::CLI::Init
|
|
83
85
|
subcommand "show", "Render units to stdout", Wheneverd::CLI::Show
|
|
84
|
-
subcommand "status", "Show systemctl
|
|
86
|
+
subcommand "status", "Show systemctl status for this identifier", Wheneverd::CLI::Status
|
|
85
87
|
subcommand "diff", "Diff rendered units vs files on disk", Wheneverd::CLI::Diff
|
|
86
88
|
subcommand "validate", "Validate schedule via systemd-analyze", Wheneverd::CLI::Validate
|
|
87
89
|
subcommand "write", "Write units to disk", Wheneverd::CLI::Write
|
|
88
90
|
subcommand "delete", "Delete units from disk", Wheneverd::CLI::Delete
|
|
89
|
-
subcommand "activate", "Enable and start timers via systemctl --user", Wheneverd::CLI::Activate
|
|
90
|
-
subcommand "deactivate", "Stop and disable timers via systemctl --user", Wheneverd::CLI::Deactivate
|
|
91
|
-
subcommand "reload", "Write units, reload daemon, restart timers", Wheneverd::CLI::Reload
|
|
91
|
+
subcommand "activate", "Enable and start timers/services via systemctl --user", Wheneverd::CLI::Activate
|
|
92
|
+
subcommand "deactivate", "Stop and disable timers/services via systemctl --user", Wheneverd::CLI::Deactivate
|
|
93
|
+
subcommand "reload", "Write units, reload daemon, restart timers/services", Wheneverd::CLI::Reload
|
|
92
94
|
subcommand "current", "Show installed units from disk", Wheneverd::CLI::Current
|
|
93
95
|
subcommand "linger", "Manage systemd user lingering via loginctl", Wheneverd::CLI::Linger
|
|
94
96
|
end
|
|
@@ -75,6 +75,37 @@ module Wheneverd
|
|
|
75
75
|
command([shell_executable, "-lc", script_stripped])
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Add a long-running systemd user service to the schedule.
|
|
79
|
+
#
|
|
80
|
+
# @example Shell service
|
|
81
|
+
# service "worker", shell: "bundle exec bin/worker"
|
|
82
|
+
#
|
|
83
|
+
# @example Argv service
|
|
84
|
+
# service "worker", command: ["bundle", "exec", "bin/worker"]
|
|
85
|
+
#
|
|
86
|
+
# @param name [String] stable service name within the schedule
|
|
87
|
+
# @param command [String, Array<String>, nil]
|
|
88
|
+
# @param shell [String, nil] shell script to run via /bin/bash -lc
|
|
89
|
+
# @param restart [String]
|
|
90
|
+
# @param restart_sec [String]
|
|
91
|
+
# @param service [Hash, Array<String>] extra [Service] lines
|
|
92
|
+
# @return [Wheneverd::Service]
|
|
93
|
+
def service(name, command: nil, shell: nil, restart: "always", restart_sec: "5s",
|
|
94
|
+
service: {})
|
|
95
|
+
command_value = normalize_service_command(command: command, shell: shell)
|
|
96
|
+
service_obj = Wheneverd::Service.new(
|
|
97
|
+
name: name,
|
|
98
|
+
command: command_value,
|
|
99
|
+
restart: restart,
|
|
100
|
+
restart_sec: restart_sec,
|
|
101
|
+
service: service
|
|
102
|
+
)
|
|
103
|
+
schedule.add_service(service_obj)
|
|
104
|
+
service_obj
|
|
105
|
+
rescue Wheneverd::InvalidCommandError => e
|
|
106
|
+
raise LoadError.new(e.message, path: path)
|
|
107
|
+
end
|
|
108
|
+
|
|
78
109
|
private
|
|
79
110
|
|
|
80
111
|
def ensure_in_every_block!(name)
|
|
@@ -102,6 +133,21 @@ module Wheneverd
|
|
|
102
133
|
stripped
|
|
103
134
|
end
|
|
104
135
|
|
|
136
|
+
def normalize_service_command(command:, shell:)
|
|
137
|
+
if command && shell
|
|
138
|
+
raise LoadError.new("service() accepts command: or shell:, not both", path: path)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return command if command
|
|
142
|
+
|
|
143
|
+
if shell
|
|
144
|
+
script = normalize_shell_script(shell)
|
|
145
|
+
return ["/bin/bash", "-lc", script]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
raise LoadError.new("service() requires command: or shell:", path: path)
|
|
149
|
+
end
|
|
150
|
+
|
|
105
151
|
def with_current_entry(entry)
|
|
106
152
|
previous_entry = @current_entry
|
|
107
153
|
@current_entry = entry
|
data/lib/wheneverd/schedule.rb
CHANGED
|
@@ -8,9 +8,14 @@ module Wheneverd
|
|
|
8
8
|
# @return [Array<Entry>]
|
|
9
9
|
attr_reader :entries
|
|
10
10
|
|
|
11
|
+
# @return [Array<Wheneverd::Service>]
|
|
12
|
+
attr_reader :services
|
|
13
|
+
|
|
11
14
|
# @param entries [Array<Entry>]
|
|
12
|
-
|
|
15
|
+
# @param services [Array<Wheneverd::Service>]
|
|
16
|
+
def initialize(entries: [], services: [])
|
|
13
17
|
@entries = entries.dup
|
|
18
|
+
@services = services.dup
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
# Append an entry to the schedule.
|
|
@@ -21,5 +26,14 @@ module Wheneverd
|
|
|
21
26
|
entries << entry
|
|
22
27
|
self
|
|
23
28
|
end
|
|
29
|
+
|
|
30
|
+
# Append a long-running service to the schedule.
|
|
31
|
+
#
|
|
32
|
+
# @param service [Wheneverd::Service]
|
|
33
|
+
# @return [Schedule] self
|
|
34
|
+
def add_service(service)
|
|
35
|
+
services << service
|
|
36
|
+
self
|
|
37
|
+
end
|
|
24
38
|
end
|
|
25
39
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wheneverd
|
|
4
|
+
# A long-running systemd user service managed alongside scheduled timers.
|
|
5
|
+
class Service
|
|
6
|
+
SAFE_SETTING_NAME = /\A[A-Za-z][A-Za-z0-9]*\z/.freeze
|
|
7
|
+
|
|
8
|
+
# @return [String]
|
|
9
|
+
attr_reader :name
|
|
10
|
+
|
|
11
|
+
# @return [Wheneverd::Job::Command]
|
|
12
|
+
attr_reader :command
|
|
13
|
+
|
|
14
|
+
# @return [String]
|
|
15
|
+
attr_reader :restart
|
|
16
|
+
|
|
17
|
+
# @return [String]
|
|
18
|
+
attr_reader :restart_sec
|
|
19
|
+
|
|
20
|
+
# @return [Array<String>]
|
|
21
|
+
attr_reader :service_lines
|
|
22
|
+
|
|
23
|
+
# @param name [String] stable service name within the schedule
|
|
24
|
+
# @param command [String, Array<String>] command to run as ExecStart
|
|
25
|
+
# @param restart [String] systemd Restart= value
|
|
26
|
+
# @param restart_sec [String] systemd RestartSec= value
|
|
27
|
+
# @param service [Hash, Array<String>] extra [Service] lines
|
|
28
|
+
def initialize(name:, command:, restart: "always", restart_sec: "5s", service: {})
|
|
29
|
+
@name = normalize_name(name)
|
|
30
|
+
@command = Wheneverd::Job::Command.new(command: command)
|
|
31
|
+
@restart = normalize_required_value(restart, "restart")
|
|
32
|
+
@restart_sec = normalize_required_value(restart_sec, "restart_sec")
|
|
33
|
+
@service_lines = normalize_service_lines(service)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Stable signature used for unit naming.
|
|
37
|
+
#
|
|
38
|
+
# @return [String]
|
|
39
|
+
def signature
|
|
40
|
+
[
|
|
41
|
+
"service:#{name}",
|
|
42
|
+
command.signature,
|
|
43
|
+
"restart:#{restart}",
|
|
44
|
+
"restart_sec:#{restart_sec}",
|
|
45
|
+
*service_lines
|
|
46
|
+
].join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def normalize_name(value)
|
|
52
|
+
str = value.to_s.strip
|
|
53
|
+
raise InvalidCommandError, "Service name must not be empty" if str.empty?
|
|
54
|
+
|
|
55
|
+
str
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def normalize_required_value(value, field)
|
|
59
|
+
str = value.to_s.strip
|
|
60
|
+
raise InvalidCommandError, "Service #{field} must not be empty" if str.empty?
|
|
61
|
+
|
|
62
|
+
str
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def normalize_service_lines(value)
|
|
66
|
+
case value
|
|
67
|
+
when Hash
|
|
68
|
+
value.map { |key, setting| normalize_service_setting(key, setting) }
|
|
69
|
+
when Array
|
|
70
|
+
value.map { |line| normalize_service_line(line) }
|
|
71
|
+
else
|
|
72
|
+
raise InvalidCommandError, "Service extra settings must be a Hash or Array"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_service_setting(key, value)
|
|
77
|
+
setting_name = key.to_s.strip
|
|
78
|
+
unless SAFE_SETTING_NAME.match?(setting_name)
|
|
79
|
+
raise InvalidCommandError, "Invalid service setting name: #{setting_name.inspect}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
"#{setting_name}=#{normalize_service_setting_value(value)}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def normalize_service_setting_value(value)
|
|
86
|
+
str = value.to_s.strip
|
|
87
|
+
raise InvalidCommandError, "Service setting values must not be empty" if str.empty?
|
|
88
|
+
if str.match?(/[\0\r\n]/)
|
|
89
|
+
raise InvalidCommandError, "Service setting values must not include NUL or newlines"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
str
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def normalize_service_line(line)
|
|
96
|
+
str = line.to_s.strip
|
|
97
|
+
raise InvalidCommandError, "Service lines must not be empty" if str.empty?
|
|
98
|
+
if str.match?(/[\0\r\n]/) || !str.include?("=")
|
|
99
|
+
raise InvalidCommandError, "Service lines must be single KEY=VALUE lines"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
str
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -10,7 +10,7 @@ module Wheneverd
|
|
|
10
10
|
# @return [Symbol] `:service` or `:timer`
|
|
11
11
|
# @!attribute [r] contents
|
|
12
12
|
# @return [String] unit file contents
|
|
13
|
-
Unit = Struct.new(:path_basename, :kind, :contents, keyword_init: true)
|
|
13
|
+
Unit = Struct.new(:path_basename, :kind, :contents, :activation, keyword_init: true)
|
|
14
14
|
|
|
15
15
|
# Renders a {Wheneverd::Schedule} into systemd units.
|
|
16
16
|
#
|
|
@@ -52,7 +52,7 @@ module Wheneverd
|
|
|
52
52
|
stable_ids = Wheneverd::Systemd::UnitNamer.stable_ids_for(schedule)
|
|
53
53
|
stable_id_index = 0
|
|
54
54
|
|
|
55
|
-
schedule.entries.flat_map do |entry|
|
|
55
|
+
units = schedule.entries.flat_map do |entry|
|
|
56
56
|
entry.jobs.flat_map do |job|
|
|
57
57
|
stable_id = stable_ids.fetch(stable_id_index)
|
|
58
58
|
stable_id_index += 1
|
|
@@ -60,9 +60,20 @@ module Wheneverd
|
|
|
60
60
|
render_job(base, entry.trigger, job)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
|
+
units.concat(render_services(schedule.services, id))
|
|
64
|
+
units
|
|
63
65
|
end
|
|
64
66
|
private_class_method :render_schedule
|
|
65
67
|
|
|
68
|
+
def self.render_services(services, id)
|
|
69
|
+
services.map do |service|
|
|
70
|
+
stable_id = Wheneverd::Systemd::UnitNamer.stable_id_for(service.signature)
|
|
71
|
+
base = "wheneverd-#{id}-#{stable_id}"
|
|
72
|
+
build_standalone_service_unit("#{base}.service", service)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
private_class_method :render_services
|
|
76
|
+
|
|
66
77
|
def self.render_job(base, trigger, job)
|
|
67
78
|
service = build_service_unit("#{base}.service", job)
|
|
68
79
|
timer = build_timer_unit("#{base}.timer", trigger)
|
|
@@ -74,17 +85,29 @@ module Wheneverd
|
|
|
74
85
|
Unit.new(
|
|
75
86
|
path_basename: path_basename,
|
|
76
87
|
kind: :service,
|
|
77
|
-
contents: UnitContentBuilder.service_contents(path_basename, job.command)
|
|
88
|
+
contents: UnitContentBuilder.service_contents(path_basename, job.command),
|
|
89
|
+
activation: :timer_managed
|
|
78
90
|
)
|
|
79
91
|
end
|
|
80
92
|
private_class_method :build_service_unit
|
|
81
93
|
|
|
94
|
+
def self.build_standalone_service_unit(path_basename, service)
|
|
95
|
+
Unit.new(
|
|
96
|
+
path_basename: path_basename,
|
|
97
|
+
kind: :service,
|
|
98
|
+
contents: UnitContentBuilder.standalone_service_contents(path_basename, service),
|
|
99
|
+
activation: :service
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
private_class_method :build_standalone_service_unit
|
|
103
|
+
|
|
82
104
|
def self.build_timer_unit(path_basename, trigger)
|
|
83
105
|
timer_lines = UnitContentBuilder.timer_lines_for(trigger)
|
|
84
106
|
Unit.new(
|
|
85
107
|
path_basename: path_basename,
|
|
86
108
|
kind: :timer,
|
|
87
|
-
contents: UnitContentBuilder.timer_contents(path_basename, timer_lines)
|
|
109
|
+
contents: UnitContentBuilder.timer_contents(path_basename, timer_lines),
|
|
110
|
+
activation: :timer
|
|
88
111
|
)
|
|
89
112
|
end
|
|
90
113
|
private_class_method :build_timer_unit
|
|
@@ -22,6 +22,29 @@ module Wheneverd
|
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Build a long-running service unit file.
|
|
26
|
+
#
|
|
27
|
+
# @param path_basename [String] unit file name for description
|
|
28
|
+
# @param service [Wheneverd::Service]
|
|
29
|
+
# @return [String] complete unit file contents
|
|
30
|
+
def self.standalone_service_contents(path_basename, service)
|
|
31
|
+
build_unit(
|
|
32
|
+
description: "wheneverd service #{path_basename}",
|
|
33
|
+
sections: [
|
|
34
|
+
"[Service]",
|
|
35
|
+
"Type=simple",
|
|
36
|
+
"ExecStart=#{service.command.command}",
|
|
37
|
+
"Restart=#{service.restart}",
|
|
38
|
+
"RestartSec=#{service.restart_sec}",
|
|
39
|
+
*service.service_lines,
|
|
40
|
+
"",
|
|
41
|
+
"[Install]",
|
|
42
|
+
"WantedBy=default.target",
|
|
43
|
+
""
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
25
48
|
# Build timer unit file contents.
|
|
26
49
|
#
|
|
27
50
|
# @param path_basename [String] unit file name for description
|
data/lib/wheneverd/version.rb
CHANGED
data/lib/wheneverd.rb
CHANGED
|
@@ -19,6 +19,7 @@ require_relative "wheneverd/duration"
|
|
|
19
19
|
require_relative "wheneverd/interval"
|
|
20
20
|
require_relative "wheneverd/core_ext/numeric_duration"
|
|
21
21
|
require_relative "wheneverd/job/command"
|
|
22
|
+
require_relative "wheneverd/service"
|
|
22
23
|
require_relative "wheneverd/trigger/base"
|
|
23
24
|
require_relative "wheneverd/trigger/interval"
|
|
24
25
|
require_relative "wheneverd/trigger/calendar"
|
data/test/cli_activate_test.rb
CHANGED
|
@@ -37,6 +37,33 @@ class CLIActivateSuccessTest < Minitest::Test
|
|
|
37
37
|
includes: expected_timer_basenames)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
|
+
|
|
41
|
+
def test_runs_enable_now_for_standalone_services
|
|
42
|
+
with_service_project_dir do
|
|
43
|
+
status, out, err, calls = run_activate_with_capture3_stub
|
|
44
|
+
assert_cli_success(status, err)
|
|
45
|
+
service = expected_standalone_service_basenames.fetch(0)
|
|
46
|
+
assert_includes out, service
|
|
47
|
+
assert_systemctl_call_starts_with(calls, 1, SYSTEMCTL_USER_PREFIX + ["enable", "--now"],
|
|
48
|
+
includes: service)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def with_service_project_dir
|
|
55
|
+
with_project_dir do
|
|
56
|
+
FileUtils.mkdir_p("config")
|
|
57
|
+
File.write("config/schedule.rb", <<~RUBY)
|
|
58
|
+
service "worker", shell: "bin/worker"
|
|
59
|
+
RUBY
|
|
60
|
+
yield
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def expected_standalone_service_basenames
|
|
65
|
+
expected_units.select { |unit| unit.activation == :service }.map(&:path_basename)
|
|
66
|
+
end
|
|
40
67
|
end
|
|
41
68
|
|
|
42
69
|
class CLIActivateScheduleMissingTest < Minitest::Test
|
data/test/cli_reload_test.rb
CHANGED
|
@@ -41,6 +41,29 @@ class CLIReloadSuccessTest < Minitest::Test
|
|
|
41
41
|
includes: expected_timer_basenames)
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
def test_restarts_standalone_services
|
|
46
|
+
with_service_project_dir do |project_dir|
|
|
47
|
+
unit_dir = File.join(project_dir, "tmp_units")
|
|
48
|
+
status, _out, err, calls = run_reload_with_capture3_stub(unit_dir: unit_dir)
|
|
49
|
+
assert_cli_success(status, err)
|
|
50
|
+
service = expected_units.find { |unit| unit.activation == :service }.path_basename
|
|
51
|
+
assert_systemctl_call_starts_with(calls, 1, SYSTEMCTL_USER_PREFIX + ["restart"],
|
|
52
|
+
includes: service)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def with_service_project_dir
|
|
59
|
+
with_project_dir do |project_dir|
|
|
60
|
+
FileUtils.mkdir_p("config")
|
|
61
|
+
File.write("config/schedule.rb", <<~RUBY)
|
|
62
|
+
service "worker", shell: "bin/worker"
|
|
63
|
+
RUBY
|
|
64
|
+
yield project_dir
|
|
65
|
+
end
|
|
66
|
+
end
|
|
44
67
|
end
|
|
45
68
|
|
|
46
69
|
class CLIReloadScheduleMissingTest < Minitest::Test
|
data/test/cli_status_test.rb
CHANGED
|
@@ -6,11 +6,11 @@ require_relative "support/cli_test_helpers"
|
|
|
6
6
|
class CLIStatusTest < Minitest::Test
|
|
7
7
|
include CLITestHelpers
|
|
8
8
|
|
|
9
|
-
def
|
|
9
|
+
def test_runs_list_timers_and_status_for_each_installed_unit
|
|
10
10
|
with_installed_units do |unit_dir|
|
|
11
11
|
status, _out, err, calls = run_status(unit_dir)
|
|
12
12
|
assert_cli_success(status, err)
|
|
13
|
-
assert_status_calls(calls, expected_timer_units)
|
|
13
|
+
assert_status_calls(calls, expected_timer_units, expected_service_units)
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -39,6 +39,9 @@ class CLIStatusTest < Minitest::Test
|
|
|
39
39
|
|
|
40
40
|
def with_installed_units
|
|
41
41
|
with_inited_project_dir do |project_dir|
|
|
42
|
+
File.open(File.join(project_dir, "config", "schedule.rb"), "a") do |file|
|
|
43
|
+
file.puts 'service "worker", shell: "bin/worker"'
|
|
44
|
+
end
|
|
42
45
|
unit_dir = File.join(project_dir, "tmp_units")
|
|
43
46
|
assert_equal 0, run_cli(["write", "--identifier", "demo", "--unit-dir", unit_dir]).first
|
|
44
47
|
yield unit_dir
|
|
@@ -53,9 +56,16 @@ class CLIStatusTest < Minitest::Test
|
|
|
53
56
|
expected_timer_basenames(identifier: "demo").sort
|
|
54
57
|
end
|
|
55
58
|
|
|
56
|
-
def
|
|
59
|
+
def expected_service_units
|
|
60
|
+
expected_units(identifier: "demo")
|
|
61
|
+
.select { |unit| unit.activation == :service }
|
|
62
|
+
.map(&:path_basename)
|
|
63
|
+
.sort
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def assert_status_calls(calls, expected_timers, expected_services)
|
|
57
67
|
assert_list_timers_call(calls, expected_timers)
|
|
58
|
-
assert_status_unit_calls(calls, expected_timers)
|
|
68
|
+
assert_status_unit_calls(calls, (expected_timers + expected_services).sort)
|
|
59
69
|
end
|
|
60
70
|
|
|
61
71
|
def assert_list_timers_call(calls, expected_timers)
|
|
@@ -15,6 +15,37 @@ class DSLContextShellTest < Minitest::Test
|
|
|
15
15
|
assert_includes error.message, "shell() script must be a String"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def test_service_accepts_shell_command
|
|
19
|
+
ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
|
|
20
|
+
ctx.service("live-poller", shell: "bundle exec bin/live")
|
|
21
|
+
|
|
22
|
+
service = ctx.schedule.services.fetch(0)
|
|
23
|
+
assert_equal "live-poller", service.name
|
|
24
|
+
assert_equal ["/bin/bash", "-lc", "bundle exec bin/live"], service.command.argv
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_service_requires_command_or_shell
|
|
28
|
+
ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
|
|
29
|
+
error = assert_raises(Wheneverd::DSL::LoadError) { ctx.service("live-poller") }
|
|
30
|
+
assert_includes error.message, "service() requires command: or shell:"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_service_rejects_command_and_shell_together
|
|
34
|
+
ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
|
|
35
|
+
error = assert_raises(Wheneverd::DSL::LoadError) do
|
|
36
|
+
ctx.service("live-poller", command: "bin/live", shell: "bin/live")
|
|
37
|
+
end
|
|
38
|
+
assert_includes error.message, "service() accepts command: or shell:, not both"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_service_wraps_validation_errors_as_load_errors
|
|
42
|
+
ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
|
|
43
|
+
error = assert_raises(Wheneverd::DSL::LoadError) do
|
|
44
|
+
ctx.service("live-poller", command: "bin/live", service: { "Bad-Key" => "1" })
|
|
45
|
+
end
|
|
46
|
+
assert_includes error.message, "Invalid service setting name"
|
|
47
|
+
end
|
|
48
|
+
|
|
18
49
|
def test_shell_rejects_empty_script
|
|
19
50
|
ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
|
|
20
51
|
error = assert_raises(Wheneverd::DSL::LoadError) { ctx.every("1m") { shell(" ") } }
|
|
@@ -37,11 +37,84 @@ module SystemdRendererTestHelpers
|
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def render_schedule(schedule, identifier: "demo")
|
|
41
|
+
Wheneverd::Systemd::Renderer.render(schedule, identifier: identifier)
|
|
42
|
+
end
|
|
43
|
+
|
|
40
44
|
def marker
|
|
41
45
|
"# Generated by wheneverd (wheneverd) #{Wheneverd::VERSION}; do not edit."
|
|
42
46
|
end
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
class SystemdRendererServiceTest < Minitest::Test
|
|
50
|
+
include SystemdRendererTestHelpers
|
|
51
|
+
|
|
52
|
+
def test_standalone_service_contains_restart_and_extra_service_settings
|
|
53
|
+
service = Wheneverd::Service.new(
|
|
54
|
+
name: "live-poller",
|
|
55
|
+
command: ["/bin/bash", "-lc", "bundle exec bin/live"],
|
|
56
|
+
restart: "always",
|
|
57
|
+
restart_sec: "5s",
|
|
58
|
+
service: { "Nice" => "10", "CPUQuota" => "80%" }
|
|
59
|
+
)
|
|
60
|
+
units = render_schedule(Wheneverd::Schedule.new(services: [service]))
|
|
61
|
+
unit = units.fetch(0)
|
|
62
|
+
|
|
63
|
+
assert_equal :service, unit.kind
|
|
64
|
+
assert_equal :service, unit.activation
|
|
65
|
+
assert_includes unit.contents, marker
|
|
66
|
+
assert_includes unit.contents, "Description=wheneverd service #{unit.path_basename}"
|
|
67
|
+
assert_includes unit.contents, "Type=simple"
|
|
68
|
+
assert_includes unit.contents, 'ExecStart=/bin/bash -lc "bundle exec bin/live"'
|
|
69
|
+
assert_includes unit.contents, "Restart=always"
|
|
70
|
+
assert_includes unit.contents, "RestartSec=5s"
|
|
71
|
+
assert_includes unit.contents, "Nice=10"
|
|
72
|
+
assert_includes unit.contents, "CPUQuota=80%"
|
|
73
|
+
assert_includes unit.contents, "WantedBy=default.target"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_standalone_service_accepts_array_service_lines
|
|
77
|
+
service = Wheneverd::Service.new(
|
|
78
|
+
name: "live-poller",
|
|
79
|
+
command: "bin/live",
|
|
80
|
+
service: ["Environment=RAILS_ENV=production"]
|
|
81
|
+
)
|
|
82
|
+
unit = render_schedule(Wheneverd::Schedule.new(services: [service])).fetch(0)
|
|
83
|
+
|
|
84
|
+
assert_includes unit.contents, "Environment=RAILS_ENV=production"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_standalone_service_rejects_invalid_service_lines
|
|
88
|
+
assert_raises(Wheneverd::InvalidCommandError) do
|
|
89
|
+
Wheneverd::Service.new(name: "live-poller", command: "bin/live", service: ["NoEquals"])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_standalone_service_rejects_invalid_service_settings_collection
|
|
94
|
+
assert_raises(Wheneverd::InvalidCommandError) do
|
|
95
|
+
Wheneverd::Service.new(name: "live-poller", command: "bin/live", service: "Nice=10")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_standalone_service_rejects_newlines_in_setting_values
|
|
100
|
+
assert_raises(Wheneverd::InvalidCommandError) do
|
|
101
|
+
Wheneverd::Service.new(name: "live-poller", command: "bin/live",
|
|
102
|
+
service: { "Environment" => "A=1\nB=2" })
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_standalone_service_naming_is_stable
|
|
107
|
+
service = Wheneverd::Service.new(name: "worker", command: "bin/worker")
|
|
108
|
+
schedule = Wheneverd::Schedule.new(services: [service])
|
|
109
|
+
|
|
110
|
+
units1 = render_schedule(schedule)
|
|
111
|
+
units2 = render_schedule(schedule)
|
|
112
|
+
|
|
113
|
+
assert_equal units1.map(&:path_basename), units2.map(&:path_basename)
|
|
114
|
+
assert_match(/\Awheneverd-demo-[0-9a-f]{12}\.service\z/, units1.fetch(0).path_basename)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
45
118
|
class SystemdRendererIntervalTest < Minitest::Test
|
|
46
119
|
include SystemdRendererTestHelpers
|
|
47
120
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wheneverd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- bigcurl
|
|
@@ -32,6 +32,7 @@ extra_rdoc_files: []
|
|
|
32
32
|
files:
|
|
33
33
|
- ".gitignore"
|
|
34
34
|
- ".rubocop.yml"
|
|
35
|
+
- ".ruby-version"
|
|
35
36
|
- ".yardopts"
|
|
36
37
|
- AGENTS.md
|
|
37
38
|
- CHANGELOG.md
|
|
@@ -78,6 +79,7 @@ files:
|
|
|
78
79
|
- lib/wheneverd/interval.rb
|
|
79
80
|
- lib/wheneverd/job/command.rb
|
|
80
81
|
- lib/wheneverd/schedule.rb
|
|
82
|
+
- lib/wheneverd/service.rb
|
|
81
83
|
- lib/wheneverd/systemd/analyze.rb
|
|
82
84
|
- lib/wheneverd/systemd/calendar_spec.rb
|
|
83
85
|
- lib/wheneverd/systemd/cron_parser.rb
|
|
@@ -154,7 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
154
156
|
- !ruby/object:Gem::Version
|
|
155
157
|
version: '0'
|
|
156
158
|
requirements: []
|
|
157
|
-
rubygems_version: 4.0.
|
|
159
|
+
rubygems_version: 4.0.11
|
|
158
160
|
specification_version: 4
|
|
159
161
|
summary: Wheneverd is to systemd timers what whenever is to cron.
|
|
160
162
|
test_files: []
|