wheneverd 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9af22418de7ea3db50ee8a05d5614f172154a7d6b1b750017594b6202aa5298
4
- data.tar.gz: 77c113c6dc437c94f59ef019cd0c274ecd94c9af2463fcfea712bbd87b109387
3
+ metadata.gz: 48c6bdb7f18c775a63b874f0509d276f9f5a9b585193aa9f3b1795f3374f26c8
4
+ data.tar.gz: ef19f548714b7c70998e7e53ee560876742108199f8418e342a7c2f7117fe796
5
5
  SHA512:
6
- metadata.gz: 2f27bd8f52c7d9e111d8b3cdcb1170b1f584f30db2e601c3938b67db29d2d1ca433e419c3020dbe01636e7e62d1798a987173deb7d1d8c3f14be91f5abf0f203
7
- data.tar.gz: 6cbacc2cc72b97b1dee8dd196c1bb332c47c8a0da0cd9e01473abf8cb799b96a6a9d2c3484d7294ae5b70fc392a6287c1ba55a6ad364547ce59387d7224c82a1
6
+ metadata.gz: 5e19a91cc2c5ad5ecd4d040cdc450de1153a8f685ef1c8be7db022d6f4189f628d8c9effe52d2a3428734a4de088e5eb1b4fba5d7d1a655fb9e38b6110794d60
7
+ data.tar.gz: 027fabc2a861d96f3f79afe6930d18758aaeaec2e39490ab93a8b7b0939a5ff543de0f6ac06438ce6ac100b68a4dd8375d0dcedf104d82f90618368bd82c7621
data/CHANGELOG.md CHANGED
@@ -5,9 +5,9 @@ On release, entries are moved into `## x.y.z` sections that match the gem versio
5
5
 
6
6
  ## Unreleased
7
7
 
8
- - No changes yet.
8
+ - Adds `wheneverd linger enable|disable|status` for managing systemd user lingering via `loginctl`.
9
9
 
10
- ## 0.1.0
10
+ ## 0.2.0
11
11
 
12
12
  - Provides a Clamp-based `wheneverd` CLI with `--help`, `--version`, and `--verbose` (usage errors in `ERROR: ...` format).
13
13
  - Adds core domain objects and helpers for building schedules (interval parsing, durations, triggers, entries, jobs).
data/FEATURE_SUMMARY.md CHANGED
@@ -12,9 +12,9 @@ It complements [`CHANGELOG.md`](CHANGELOG.md) by staying high-level and focusing
12
12
 
13
13
  ## Unreleased
14
14
 
15
- - No changes yet.
15
+ - Adds `wheneverd linger enable|disable|status` for managing systemd user lingering via `loginctl`.
16
16
 
17
- ## 0.1.0
17
+ ## 0.2.0
18
18
 
19
19
  - The `wheneverd` CLI is implemented using Clamp (`--help`, usage errors in `ERROR: ...` format, `--verbose` for details).
20
20
  - The gem includes a small “whenever-like” domain model (interval parsing, durations, triggers, schedules).
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wheneverd (0.1.0)
4
+ wheneverd (0.2.0)
5
5
  clamp (~> 1.3)
6
6
 
7
7
  GEM
@@ -122,7 +122,7 @@ CHECKSUMS
122
122
  tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
123
123
  unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
124
124
  unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
125
- wheneverd (0.1.0)
125
+ wheneverd (0.2.0)
126
126
  yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f
127
127
 
128
128
  BUNDLED WITH
data/README.md CHANGED
@@ -38,6 +38,7 @@ wheneverd activate
38
38
  wheneverd deactivate
39
39
  wheneverd reload
40
40
  wheneverd current
41
+ wheneverd linger status
41
42
  ```
42
43
 
43
44
  ### Minimal `config/schedule.rb` example
@@ -81,6 +82,35 @@ After changing your schedule, rewrite units and restart the timer(s) to pick up
81
82
  wheneverd reload
82
83
  ```
83
84
 
85
+ ### User timers and lingering (`loginctl enable-linger`)
86
+
87
+ By default, `wheneverd` uses *user* systemd units (`systemctl --user`). On many systems, the per-user systemd instance
88
+ only runs while you are logged in. If you want timers to run after logout (or on boot without an interactive login),
89
+ enable lingering for your user:
90
+
91
+ ```bash
92
+ wheneverd linger enable
93
+ ```
94
+
95
+ This runs `loginctl enable-linger "$USER"` under the hood. If you see “Access denied”, your system may require admin
96
+ privileges (polkit policy); try:
97
+
98
+ ```bash
99
+ sudo loginctl enable-linger "$USER"
100
+ ```
101
+
102
+ Check whether lingering is enabled:
103
+
104
+ ```bash
105
+ wheneverd linger status
106
+ ```
107
+
108
+ To disable it later:
109
+
110
+ ```bash
111
+ wheneverd linger disable
112
+ ```
113
+
84
114
  ## Syntax
85
115
 
86
116
  Schedules are defined in a Ruby file (default: `config/schedule.rb`) and evaluated in a dedicated DSL context.
@@ -190,6 +220,10 @@ Commands:
190
220
 
191
221
  ```bash
192
222
  bundle install
223
+
224
+ # Run the CLI from this repo:
225
+ bundle exec exe/wheneverd --help
226
+
193
227
  bundle exec rake test
194
228
  bundle exec rake ci
195
229
  bundle exec rake yard
data/exe/wheneverd CHANGED
@@ -1,9 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ begin
5
+ require "bundler/setup"
6
+ rescue LoadError
7
+ # Allow running without Bundler (e.g., when installed as a gem).
8
+ end
9
+
4
10
  lib = File.expand_path("../lib", __dir__)
5
11
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
12
 
7
- require "wheneverd/cli"
13
+ begin
14
+ require "wheneverd/cli"
15
+ rescue LoadError => e
16
+ if e.message.include?("clamp")
17
+ warn "ERROR: missing gem dependency `clamp`."
18
+ warn "Run `bundle install` and then `bundle exec exe/wheneverd ...`."
19
+ exit 1
20
+ end
21
+ raise
22
+ end
8
23
 
9
24
  exit(Wheneverd::CLI.run || 0)
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ # Implements `wheneverd linger` (manage systemd user lingering via `loginctl`).
5
+ class CLI::Linger < CLI
6
+ option "--user", "NAME", "Username to manage (default: $USER)"
7
+
8
+ private
9
+
10
+ def username_value
11
+ name = user.to_s.strip
12
+ name = ENV.fetch("USER", "").strip if name.empty?
13
+ raise ArgumentError, "Could not determine username; pass --user NAME" if name.empty?
14
+
15
+ name
16
+ end
17
+ end
18
+ end
19
+
20
+ module Wheneverd
21
+ class CLI::Linger::Enable < CLI::Linger
22
+ def execute
23
+ name = username_value
24
+ Wheneverd::Systemd::Loginctl.enable_linger(name)
25
+ puts "Enabled lingering for #{name}"
26
+ 0
27
+ rescue StandardError => e
28
+ handle_error(e)
29
+ end
30
+ end
31
+ end
32
+
33
+ module Wheneverd
34
+ class CLI::Linger::Disable < CLI::Linger
35
+ def execute
36
+ name = username_value
37
+ Wheneverd::Systemd::Loginctl.disable_linger(name)
38
+ puts "Disabled lingering for #{name}"
39
+ 0
40
+ rescue StandardError => e
41
+ handle_error(e)
42
+ end
43
+ end
44
+ end
45
+
46
+ module Wheneverd
47
+ class CLI::Linger::Status < CLI::Linger
48
+ def execute
49
+ name = username_value
50
+ stdout, _stderr = Wheneverd::Systemd::Loginctl.show_linger(name)
51
+ puts stdout.strip unless stdout.to_s.strip.empty?
52
+ 0
53
+ rescue StandardError => e
54
+ handle_error(e)
55
+ end
56
+ end
57
+ end
58
+
59
+ module Wheneverd
60
+ class CLI::Linger
61
+ self.default_subcommand = "status"
62
+
63
+ subcommand "enable", "Enable lingering via loginctl", Wheneverd::CLI::Linger::Enable
64
+ subcommand "disable", "Disable lingering via loginctl", Wheneverd::CLI::Linger::Disable
65
+ subcommand "status", "Show lingering status via loginctl", Wheneverd::CLI::Linger::Status
66
+ end
67
+ end
data/lib/wheneverd/cli.rb CHANGED
@@ -69,6 +69,7 @@ require_relative "cli/activate"
69
69
  require_relative "cli/deactivate"
70
70
  require_relative "cli/reload"
71
71
  require_relative "cli/current"
72
+ require_relative "cli/linger"
72
73
 
73
74
  module Wheneverd
74
75
  class CLI
@@ -83,5 +84,6 @@ module Wheneverd
83
84
  subcommand "deactivate", "Stop and disable timers via systemctl --user", Wheneverd::CLI::Deactivate
84
85
  subcommand "reload", "Write units, reload daemon, restart timers", Wheneverd::CLI::Reload
85
86
  subcommand "current", "Show installed units from disk", Wheneverd::CLI::Current
87
+ subcommand "linger", "Manage systemd user lingering via loginctl", Wheneverd::CLI::Linger
86
88
  end
87
89
  end
@@ -19,5 +19,8 @@ module Wheneverd
19
19
 
20
20
  # Raised when a `systemctl` invocation fails.
21
21
  class SystemctlError < Error; end
22
+
23
+ # Raised when a `loginctl` invocation fails.
24
+ class LoginctlError < Error; end
22
25
  end
23
26
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Wheneverd
6
+ module Systemd
7
+ # Thin wrapper around `loginctl` that raises on non-zero exit status.
8
+ class Loginctl
9
+ # Run `loginctl` and return stdout/stderr.
10
+ #
11
+ # @param args [Array<String>]
12
+ # @return [Array(String, String)] stdout and stderr
13
+ # @raise [Wheneverd::Systemd::LoginctlError]
14
+ def self.run(*args)
15
+ cmd = ["loginctl", "--no-pager"]
16
+ cmd.concat(args.flatten.map(&:to_s))
17
+
18
+ stdout, stderr, status = Open3.capture3(*cmd)
19
+ raise LoginctlError, format_error(cmd, status, stdout, stderr) unless status.success?
20
+
21
+ [stdout, stderr]
22
+ end
23
+
24
+ # Enable lingering for a user (so their `systemctl --user` instance can run while logged out).
25
+ #
26
+ # @param username [String]
27
+ # @return [Array(String, String)] stdout and stderr
28
+ def self.enable_linger(username)
29
+ run("enable-linger", username)
30
+ end
31
+
32
+ # Disable lingering for a user.
33
+ #
34
+ # @param username [String]
35
+ # @return [Array(String, String)] stdout and stderr
36
+ def self.disable_linger(username)
37
+ run("disable-linger", username)
38
+ end
39
+
40
+ # Show lingering state for a user (includes a `Linger=` line).
41
+ #
42
+ # @param username [String]
43
+ # @return [Array(String, String)] stdout and stderr
44
+ def self.show_linger(username)
45
+ run("show-user", username, "-p", "Linger")
46
+ end
47
+
48
+ def self.format_error(cmd, status, stdout, stderr)
49
+ details = []
50
+ details << "command: #{cmd.join(' ')}"
51
+ details << "status: #{status.exitstatus}"
52
+ details << "stdout: #{stdout.strip}" unless stdout.to_s.strip.empty?
53
+ details << "stderr: #{stderr.strip}" unless stderr.to_s.strip.empty?
54
+ "loginctl failed (#{details.join(', ')})"
55
+ end
56
+ private_class_method :format_error
57
+ end
58
+ end
59
+ end
@@ -80,12 +80,10 @@ module Wheneverd
80
80
  private_class_method :build_service_unit
81
81
 
82
82
  def self.build_timer_unit(path_basename, trigger)
83
- timer_lines = timer_lines_for(trigger)
84
-
85
83
  Unit.new(
86
84
  path_basename: path_basename,
87
85
  kind: :timer,
88
- contents: timer_contents(path_basename, timer_lines)
86
+ contents: timer_contents(path_basename, timer_lines_for(trigger))
89
87
  )
90
88
  end
91
89
  private_class_method :build_timer_unit
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Wheneverd
4
4
  # Gem version.
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
data/lib/wheneverd.rb CHANGED
@@ -36,6 +36,7 @@ require_relative "wheneverd/systemd/calendar_spec"
36
36
  require_relative "wheneverd/systemd/unit_namer"
37
37
  require_relative "wheneverd/systemd/renderer"
38
38
  require_relative "wheneverd/systemd/systemctl"
39
+ require_relative "wheneverd/systemd/loginctl"
39
40
  require_relative "wheneverd/systemd/unit_writer"
40
41
  require_relative "wheneverd/systemd/unit_deleter"
41
42
  require_relative "wheneverd/systemd/unit_lister"
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require_relative "support/cli_test_helpers"
5
+
6
+ class CLILingerEnableTest < Minitest::Test
7
+ include CLITestHelpers
8
+
9
+ def test_exits_zero
10
+ with_project_dir do
11
+ status, _out, err, _calls = run_cli_with_capture3_stub(["linger", "enable", "--user", "demo"])
12
+ assert_cli_success(status, err)
13
+ end
14
+ end
15
+
16
+ def test_calls_loginctl_enable_linger
17
+ with_project_dir do
18
+ status, _out, err, calls = run_cli_with_capture3_stub(["linger", "enable", "--user", "demo"])
19
+ assert_cli_success(status, err)
20
+ args, kwargs = calls.fetch(0)
21
+ assert_equal ["loginctl", "--no-pager", "enable-linger", "demo"], args
22
+ assert_equal({}, kwargs)
23
+ end
24
+ end
25
+
26
+ def test_prints_confirmation
27
+ with_project_dir do
28
+ status, out, err, _calls = run_cli_with_capture3_stub(["linger", "enable", "--user", "demo"])
29
+ assert_cli_success(status, err)
30
+ assert_includes out, "Enabled lingering for demo"
31
+ end
32
+ end
33
+ end
34
+
35
+ class CLILingerDisableTest < Minitest::Test
36
+ include CLITestHelpers
37
+
38
+ def test_calls_loginctl_disable_linger
39
+ with_project_dir do
40
+ status, _out, err, calls = run_cli_with_capture3_stub(["linger", "disable", "--user", "demo"])
41
+ assert_cli_success(status, err)
42
+ args, _kwargs = calls.fetch(0)
43
+ assert_equal ["loginctl", "--no-pager", "disable-linger", "demo"], args
44
+ end
45
+ end
46
+ end
47
+
48
+ class CLILingerStatusTest < Minitest::Test
49
+ include CLITestHelpers
50
+
51
+ def test_is_default_subcommand
52
+ with_project_dir do
53
+ status, _out, err, calls = run_cli_with_capture3_stub(["linger", "--user", "demo"],
54
+ stdout: "Linger=yes\n")
55
+ assert_cli_success(status, err)
56
+ args, _kwargs = calls.fetch(0)
57
+ assert_equal ["loginctl", "--no-pager", "show-user", "demo", "-p", "Linger"], args
58
+ end
59
+ end
60
+
61
+ def test_prints_linger_property_line
62
+ with_project_dir do
63
+ status, out, err, _calls = run_cli_with_capture3_stub(
64
+ ["linger", "status", "--user", "demo"],
65
+ stdout: "Linger=yes\n"
66
+ )
67
+ assert_cli_success(status, err)
68
+ assert_equal "Linger=yes\n", out
69
+ end
70
+ end
71
+ end
72
+
73
+ class CLILingerFailureTest < Minitest::Test
74
+ include CLITestHelpers
75
+
76
+ def test_exits_one_on_access_denied
77
+ with_project_dir do
78
+ status, _out, _err, _calls = run_cli_with_capture3_stub(
79
+ ["linger", "enable", "--user", "demo"],
80
+ exitstatus: 1,
81
+ stderr: "Access denied\n"
82
+ )
83
+ assert_equal 1, status
84
+ end
85
+ end
86
+
87
+ def test_prints_loginctl_failed_message
88
+ with_project_dir do
89
+ _status, out, err, _calls = run_cli_with_capture3_stub(
90
+ ["linger", "enable", "--user", "demo"],
91
+ exitstatus: 1,
92
+ stderr: "Access denied\n"
93
+ )
94
+ assert_equal "", out
95
+ assert_includes err, "loginctl failed"
96
+ assert_includes err, "Access denied"
97
+ end
98
+ end
99
+
100
+ def test_disable_failure_uses_handle_error
101
+ with_project_dir do
102
+ status, out, err, _calls = run_cli_with_capture3_stub(
103
+ ["linger", "disable", "--user", "demo"],
104
+ exitstatus: 1,
105
+ stderr: "Access denied\n"
106
+ )
107
+ assert_equal 1, status
108
+ assert_equal "", out
109
+ assert_includes err, "loginctl failed"
110
+ end
111
+ end
112
+
113
+ def test_status_failure_uses_handle_error
114
+ with_project_dir do
115
+ status, out, err, _calls = run_cli_with_capture3_stub(
116
+ ["linger", "status", "--user", "demo"],
117
+ exitstatus: 1,
118
+ stderr: "Access denied\n"
119
+ )
120
+ assert_equal 1, status
121
+ assert_equal "", out
122
+ assert_includes err, "loginctl failed"
123
+ end
124
+ end
125
+ end
data/wheneverd.gemspec CHANGED
@@ -5,8 +5,7 @@ require_relative "lib/wheneverd/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "wheneverd"
7
7
  spec.version = Wheneverd::VERSION
8
- spec.authors = ["Dr. Samuel Goebert"]
9
- spec.email = ["maintainers@example.com"]
8
+ spec.authors = ["bigcurl"]
10
9
 
11
10
  spec.summary = "Wheneverd is to systemd timers what whenever is to cron."
12
11
  spec.description =
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wheneverd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - Dr. Samuel Goebert
7
+ - bigcurl
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -25,8 +25,6 @@ dependencies:
25
25
  version: '1.3'
26
26
  description: Generates systemd timer/service units from a Ruby DSL, similar in spirit
27
27
  to the whenever gem for cron.
28
- email:
29
- - maintainers@example.com
30
28
  executables:
31
29
  - wheneverd
32
30
  extensions: []
@@ -54,6 +52,7 @@ files:
54
52
  - lib/wheneverd/cli/delete.rb
55
53
  - lib/wheneverd/cli/help.rb
56
54
  - lib/wheneverd/cli/init.rb
55
+ - lib/wheneverd/cli/linger.rb
57
56
  - lib/wheneverd/cli/reload.rb
58
57
  - lib/wheneverd/cli/show.rb
59
58
  - lib/wheneverd/cli/write.rb
@@ -73,6 +72,7 @@ files:
73
72
  - lib/wheneverd/systemd/calendar_spec.rb
74
73
  - lib/wheneverd/systemd/cron_parser.rb
75
74
  - lib/wheneverd/systemd/errors.rb
75
+ - lib/wheneverd/systemd/loginctl.rb
76
76
  - lib/wheneverd/systemd/renderer.rb
77
77
  - lib/wheneverd/systemd/systemctl.rb
78
78
  - lib/wheneverd/systemd/time_parser.rb
@@ -88,6 +88,7 @@ files:
88
88
  - test/cli_current_test.rb
89
89
  - test/cli_deactivate_test.rb
90
90
  - test/cli_end_to_end_test.rb
91
+ - test/cli_linger_test.rb
91
92
  - test/cli_reload_test.rb
92
93
  - test/cli_systemctl_integration_test.rb
93
94
  - test/cli_systemd_analyze_test.rb