wheneverd 0.3.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile.lock +38 -33
  5. data/README.md +64 -7
  6. data/lib/wheneverd/cli/activate.rb +5 -5
  7. data/lib/wheneverd/cli/deactivate.rb +6 -6
  8. data/lib/wheneverd/cli/reload.rb +8 -8
  9. data/lib/wheneverd/cli/status.rb +13 -6
  10. data/lib/wheneverd/cli.rb +10 -8
  11. data/lib/wheneverd/dsl/context.rb +46 -0
  12. data/lib/wheneverd/dsl/period_parser.rb +8 -107
  13. data/lib/wheneverd/dsl/period_strategy/array_strategy.rb +29 -0
  14. data/lib/wheneverd/dsl/period_strategy/base.rb +65 -0
  15. data/lib/wheneverd/dsl/period_strategy/duration_strategy.rb +33 -0
  16. data/lib/wheneverd/dsl/period_strategy/string_strategy.rb +51 -0
  17. data/lib/wheneverd/dsl/period_strategy/symbol_strategy.rb +31 -0
  18. data/lib/wheneverd/dsl/period_strategy.rb +43 -0
  19. data/lib/wheneverd/duration.rb +1 -7
  20. data/lib/wheneverd/errors.rb +3 -0
  21. data/lib/wheneverd/interval.rb +22 -7
  22. data/lib/wheneverd/schedule.rb +15 -1
  23. data/lib/wheneverd/service.rb +105 -0
  24. data/lib/wheneverd/systemd/cron_parser/dow_parser.rb +208 -0
  25. data/lib/wheneverd/systemd/cron_parser/field_parser.rb +163 -0
  26. data/lib/wheneverd/systemd/cron_parser.rb +56 -303
  27. data/lib/wheneverd/systemd/renderer.rb +31 -66
  28. data/lib/wheneverd/systemd/unit_content_builder.rb +99 -0
  29. data/lib/wheneverd/systemd/unit_deleter.rb +2 -28
  30. data/lib/wheneverd/systemd/unit_lister.rb +2 -28
  31. data/lib/wheneverd/systemd/unit_namer.rb +6 -15
  32. data/lib/wheneverd/systemd/unit_path_utils.rb +54 -0
  33. data/lib/wheneverd/systemd/unit_writer.rb +2 -28
  34. data/lib/wheneverd/trigger/base.rb +22 -0
  35. data/lib/wheneverd/trigger/boot.rb +8 -6
  36. data/lib/wheneverd/trigger/calendar.rb +7 -0
  37. data/lib/wheneverd/trigger/interval.rb +8 -6
  38. data/lib/wheneverd/validation.rb +89 -0
  39. data/lib/wheneverd/version.rb +1 -1
  40. data/lib/wheneverd.rb +5 -1
  41. data/test/cli_activate_test.rb +27 -0
  42. data/test/cli_reload_test.rb +23 -0
  43. data/test/cli_status_test.rb +14 -4
  44. data/test/domain_model_test.rb +105 -0
  45. data/test/dsl_context_shell_test.rb +31 -0
  46. data/test/systemd_cron_parser_test.rb +41 -25
  47. data/test/systemd_renderer_errors_test.rb +1 -1
  48. data/test/systemd_renderer_test.rb +73 -0
  49. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c4d813e1e0ad0b44e738d4ba42767b5fb490dcaf5a7883328c91faa2ab726305
4
- data.tar.gz: 1185929834d9f12adb4afd8a1bc2d1a4a366d99b260afe1d075a1ca58dc6dc26
3
+ metadata.gz: 9469e813aa6390a8e7eb911ce672a9fdaf7ab861e435d71d2d8887d551f8b8f8
4
+ data.tar.gz: dcf0211fe96fe0faff489a82ab3d0f12f36c3703083a949606eed4ab8eb9fa28
5
5
  SHA512:
6
- metadata.gz: ed43947461d6e8dadf5825c8075e08816535c42202a9764c7b57d1fb4a7948a0dbfe1ff57e13cc710a853efa6910f802b93774b239736ed30bcec145812c08fc
7
- data.tar.gz: 4406adc4f70afde1953cd8ea82979d2c37bf6b096232930724f2537b04d5ae187437fafa29215163ebd985eb132b89c7ee990497ba2cdb70617166b5d88114ee
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,22 @@ 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
+
14
+ ## 0.4.0
15
+
16
+ - Docs: adds a copy/paste "deploy a simple schedule" example and refines README status section.
17
+ - Refactor: extracts `UnitPathUtils` module for shared identifier/path utilities across `UnitWriter`, `UnitDeleter`, `UnitLister`, and `Renderer`.
18
+ - Refactor: adds polymorphic `Trigger::Base` interface with `#systemd_timer_lines` and `#signature` methods for all trigger types.
19
+ - Refactor: splits `CronParser` into focused `FieldParser` and `DowParser` submodules for maintainability.
20
+ - Refactor: implements strategy pattern for `PeriodParser` with dedicated strategies for Duration, String, Symbol, and Array inputs.
21
+ - Refactor: extracts `UnitContentBuilder` from `Renderer` for cleaner separation of unit content generation.
22
+ - Refactor: adds `Validation` module with composable validators (`type`, `positive_integer`, `non_empty_string`, `non_empty_array`, `in_range`).
23
+
8
24
  ## 0.3.0
9
25
 
10
26
  - Schedule DSL: `command` accepts argv arrays, adds a `shell` helper for `/bin/bash -lc`, and `wheneverd init` includes examples.
data/Gemfile.lock CHANGED
@@ -1,61 +1,64 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wheneverd (0.3.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.3.2)
11
+ clamp (1.5.2)
12
12
  date (3.5.1)
13
13
  docile (1.4.1)
14
- erb (6.0.1)
14
+ drb (2.2.3)
15
+ erb (6.0.4)
15
16
  io-console (0.8.2)
16
- irb (1.16.0)
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.18.0)
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.1)
25
+ minitest (6.0.5)
26
+ drb (~> 2.0)
24
27
  prism (~> 1.5)
25
- parallel (1.27.0)
26
- parser (3.3.10.0)
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.7.0)
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.3.1)
39
- rdoc (7.1.0)
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.11.3)
47
+ regexp_parser (2.12.0)
45
48
  reline (0.6.3)
46
49
  io-console (~> 0.5)
47
- rubocop (1.82.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 (~> 1.10)
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.48.0, < 2.0)
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.0)
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.38)
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
- clamp (1.3.2) sha256=4f6a99a8678d51abbf1650263a74d1ac50939edc11986271431d2e03a0d7a022
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
- erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
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.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
98
- json (2.18.0) sha256=b10506aee4183f5cf49e0efc48073d7b75843ce3782c68dbeb763351c08fd505
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.1) sha256=7854c74f48e2e975969062833adc4013f249a4b212f5e7b9d5c040bf838d54bb
102
- parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
103
- parser (3.3.10.0) sha256=ce3587fa5cc55a88c4ba5b2b37621b3329aadf5728f9eafa36bbd121462aabd6
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.7.0) sha256=10062f734bf7985c8424c44fac382ac04a58124ea3d220ec3ba9fe4f2da65103
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.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
111
- rdoc (7.1.0) sha256=494899df0706c178596ca6e1d50f1b7eb285a9b2aae715be5abd742734f17363
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.11.3) sha256=ca13f381a173b7a93450e53459075c9b76a10433caadcb2f1180f2c741fc55a4
118
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
114
119
  reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
115
- rubocop (1.82.1) sha256=09f1a6a654a960eda767aebea33e47603080f8e9c9a3f019bf9b94c9cab5e273
116
- rubocop-ast (1.49.0) sha256=49c3676d3123a0923d333e20c6c2dbaaae2d2287b475273fddee0c61da9f71fd
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.3.0)
126
- yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f
130
+ wheneverd (0.5.0)
131
+ yard (0.9.43) sha256=cf8733a8f0485df2a162927e9b5f182215a61f6d22de096b8f402c726a1c5821
127
132
 
128
133
  BUNDLED WITH
129
- 4.0.3
134
+ 4.0.11
data/README.md CHANGED
@@ -4,14 +4,23 @@ Wheneverd is to systemd timers what the [`whenever` gem](https://github.com/java
4
4
 
5
5
  ## Status
6
6
 
7
- Pre-1.0, but working end-to-end for user systemd timers:
7
+ Pre-1.0, but working end-to-end for systemd user timers on Linux:
8
8
 
9
9
  - Loads a Ruby schedule DSL file (default: `config/schedule.rb`).
10
10
  - Renders systemd `.service`/`.timer` units (interval, calendar, and 5-field cron schedules).
11
- - Writes, lists, and deletes generated unit files (default: `~/.config/systemd/user`).
11
+ - Writes, diffs, shows, and deletes generated unit files (default: `~/.config/systemd/user`).
12
12
  - Enables/starts/stops/disables/restarts timers via `systemctl --user`.
13
+ - Validates `OnCalendar=` values with `systemd-analyze` (optional unit verification).
13
14
  - Manages lingering via `loginctl` (so timers can run while logged out).
14
15
 
16
+ Non-goals / not yet implemented:
17
+
18
+ - System-level units (`/etc/systemd/system`) / `systemctl` without `--user`.
19
+ - Non-systemd schedulers (cron, launchd, etc).
20
+ - Non-Linux platforms (no Windows/macOS support).
21
+
22
+ Expect the CLI and generated unit details to change until 1.0.
23
+
15
24
  See `FEATURE_SUMMARY.md` for user-visible behavior, and `CHANGELOG.md` for release notes.
16
25
 
17
26
  ## Installation
@@ -62,6 +71,40 @@ every 1.day, at: "4:30 am" do
62
71
  end
63
72
  ```
64
73
 
74
+ ### Deploy a simple schedule (copy/paste)
75
+
76
+ From your project root (the default identifier is the current directory name):
77
+
78
+ ```bash
79
+ # Install (skip if already in your Gemfile)
80
+ bundle add wheneverd
81
+ bundle install
82
+
83
+ # Write a schedule that appends a timestamp to ~/.cache/wheneverd-demo.log every minute
84
+ mkdir -p config
85
+ cat > config/schedule.rb <<'RUBY'
86
+ # frozen_string_literal: true
87
+
88
+ every "1m" do
89
+ shell "mkdir -p ~/.cache && date >> ~/.cache/wheneverd-demo.log"
90
+ end
91
+ RUBY
92
+
93
+ # Preview, write units, and enable/start the timer(s)
94
+ bundle exec wheneverd show
95
+ bundle exec wheneverd validate
96
+ bundle exec wheneverd write
97
+ bundle exec wheneverd activate
98
+
99
+ # Verify it’s installed and running
100
+ bundle exec wheneverd status
101
+ tail -n 5 ~/.cache/wheneverd-demo.log
102
+
103
+ # Stop/disable timers and remove generated unit files
104
+ bundle exec wheneverd deactivate
105
+ bundle exec wheneverd delete
106
+ ```
107
+
65
108
  Preview the generated units:
66
109
 
67
110
  ```bash
@@ -124,7 +167,7 @@ Schedules are defined in a Ruby file (default: `config/schedule.rb`) and evaluat
124
167
 
125
168
  Note: schedule files are executed as Ruby. Do not run untrusted schedule code.
126
169
 
127
- The core shape is:
170
+ The core timer shape is:
128
171
 
129
172
  ```ruby
130
173
  every(period, at: nil) do
@@ -132,6 +175,20 @@ every(period, at: nil) do
132
175
  end
133
176
  ```
134
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
+
135
192
  For calendar schedules, you can also pass multiple period symbols (or an array) to run the same jobs on multiple days:
136
193
 
137
194
  ```ruby
@@ -227,14 +284,14 @@ Commands:
227
284
 
228
285
  - `wheneverd init [--schedule PATH] [--force]` writes a template schedule file.
229
286
  - `wheneverd show [--schedule PATH] [--identifier NAME]` prints rendered units to stdout.
230
- - `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.
231
288
  - `wheneverd diff [--schedule PATH] [--identifier NAME] [--unit-dir PATH]` diffs rendered units vs unit files on disk.
232
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).
233
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).
234
291
  - `wheneverd delete [--identifier NAME] [--unit-dir PATH] [--dry-run]` deletes previously generated units for the identifier.
235
- - `wheneverd activate [--schedule PATH] [--identifier NAME]` runs `systemctl --user daemon-reload` and enables/starts the timers.
236
- - `wheneverd deactivate [--schedule PATH] [--identifier NAME]` stops and disables the timers.
237
- - `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.
238
295
  - `wheneverd current [--identifier NAME] [--unit-dir PATH]` prints the currently installed unit file contents from disk.
239
296
  - `wheneverd linger [--user NAME] [enable|disable|status]` manages lingering via `loginctl` (`status` is the default).
240
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
- timer_units = timer_unit_basenames
8
- return 0 if timer_units.empty?
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", *timer_units)
11
+ Wheneverd::Systemd::Systemctl.run("enable", "--now", *units)
12
12
 
13
- timer_units.each { |unit| puts unit }
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
- timer_units = timer_unit_basenames
8
- return 0 if timer_units.empty?
7
+ units = activatable_unit_basenames
8
+ return 0 if units.empty?
9
9
 
10
- Wheneverd::Systemd::Systemctl.run("stop", *timer_units)
11
- Wheneverd::Systemd::Systemctl.run("disable", *timer_units)
10
+ Wheneverd::Systemd::Systemctl.run("stop", *units)
11
+ Wheneverd::Systemd::Systemctl.run("disable", *units)
12
12
 
13
- timer_units.each { |unit| puts unit }
13
+ units.each { |unit| puts unit }
14
14
  0
15
15
  rescue StandardError => e
16
16
  handle_error(e)
@@ -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, timer_units = write_units_and_timer_basenames
12
- return 0 if timer_units.empty?
11
+ paths, units = write_units_and_activatable_basenames
12
+ return 0 if units.empty?
13
13
 
14
- reload_systemd(timer_units)
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 write_units_and_timer_basenames
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, timer_unit_basenames(units)]
32
+ [paths, activatable_unit_basenames(units)]
33
33
  end
34
34
 
35
- def reload_systemd(timer_units)
35
+ def reload_systemd(units)
36
36
  Wheneverd::Systemd::Systemctl.run("daemon-reload")
37
- Wheneverd::Systemd::Systemctl.run("restart", *timer_units)
37
+ Wheneverd::Systemd::Systemctl.run("restart", *units)
38
38
  end
39
39
  end
40
40
  end
@@ -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 = installed_timer_unit_basenames
8
- return 0 if timer_units.empty?
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(timer_units)
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 installed_timer_unit_basenames
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) }.grep(/\.timer\z/).uniq
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 timer_unit_basenames(units = render_units)
56
- units.select { |unit| unit.kind == :timer }.map(&:path_basename).uniq
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, :timer_unit_basenames
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 list-timers + status for this identifier", Wheneverd::CLI::Status
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