tomo 0.8.1 → 0.9.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: e355e8b9452472cd2e2da9cbbfb6d577d4bf4ae0bfdb57a16216a2b45387433b
4
- data.tar.gz: ceae2f4b9e5d766824bca43b5f44f71d3d0c2af1e2ae3f4608b33a60c41a61ae
3
+ metadata.gz: 67815fa19818dc29ff1226179db8b2b364c4843eb03c3a507561dd105b480b9f
4
+ data.tar.gz: 7119dc681c2e8e167712a1ee2a35bb7725d16f4f14b0438ec036abd7f30d42e5
5
5
  SHA512:
6
- metadata.gz: 8e36ee70864b8b0d663d7766dc83b6e4f788f7d525ff36624c96cd1f56a87f2182b8e955e87a5e93cc708253edf352d4677fbe554adab1cc33877faa100ff56c
7
- data.tar.gz: f99aa7197d92bf4e81836f2c892ac1a76b4c7580de1cf72e8f0c1ea47914dfc85504019ac80922336179c7ff62510f6fe3bfe86cf7bb7b6a1a8bae8a526bca6b
6
+ metadata.gz: 271bafaa1a657c3d5961629f07f64ff7e595ca38b20fc34abc3c747418e108dee5a2b50cc6b164ba0721a71e7779100e4af5544f3085d596d9397c8455c6c910
7
+ data.tar.gz: e8a9dfd41c8226238add4a7dbddd5b212cff0953d9eb25b2cfbaf9c435d48e741bd806fbaec76b5c7591d0073e0cd0982885ab0b7a03b70f3628fb9f0eebef5f
data/README.md CHANGED
@@ -60,7 +60,7 @@ plugin "rails"
60
60
  host "user@hostname.or.ip.address"
61
61
 
62
62
  set application: "my-rails-app"
63
- set deploy_to: "/var/www/%<application>"
63
+ set deploy_to: "/var/www/%{application}"
64
64
  set git_url: "git@github.com:my-username/my-rails-app.git"
65
65
  set git_branch: "master"
66
66
  # ...
@@ -202,20 +202,6 @@ By default, tomo uses the ["accept-new"](https://www.openssh.com/txt/release-7.6
202
202
  set ssh_strict_host_key_checking: true # or false
203
203
  ```
204
204
 
205
- #### Why does my deploy hang after starting puma?
206
-
207
- Puma 4.1.0 [has a bug](https://github.com/puma/puma/issues/1906) where its output isn't properly detached prior to daemonzing. This causes tomo to hang waiting for output. You may see something like this prior to the deploy freezing:
208
-
209
- ```
210
- Puma starting in single mode...
211
- * Version 4.1.0 (ruby 2.6.4-p104), codename: Fourth and One
212
- * Min threads: 5, max threads: 5
213
- * Environment: production
214
- * Daemonizing...
215
- ```
216
-
217
- To fix, upgrade to puma 4.1.1 or newer.
218
-
219
205
  ## Support
220
206
 
221
207
  This project is a labor of love and I can only spend a few hours a week maintaining it, at most. If you'd like to help by submitting a pull request, or if you've discovered a bug that needs my attention, please let me know. Check out [CONTRIBUTING.md](https://github.com/mattbrictson/tomo/blob/master/CONTRIBUTING.md) to get started. Happy hacking! —Matt
@@ -12,7 +12,7 @@ module Tomo::Plugin
12
12
  bundler_deployment: true,
13
13
  bundler_gemfile: nil,
14
14
  bundler_jobs: "4",
15
- bundler_path: "%<shared_path>/bundle",
15
+ bundler_path: "%{shared_path}/bundle",
16
16
  bundler_retry: "3",
17
17
  bundler_version: nil,
18
18
  bundler_without: %w[development test]
@@ -11,16 +11,16 @@ module Tomo::Plugin
11
11
  defaults Tomo::SSH::Options::DEFAULTS.merge(
12
12
  application: "default",
13
13
  concurrency: 10,
14
- current_path: "%<deploy_to>/current",
15
- deploy_to: "/var/www/%<application>",
14
+ current_path: "%{deploy_to}/current",
15
+ deploy_to: "/var/www/%{application}",
16
16
  keep_releases: 10,
17
17
  linked_dirs: [],
18
18
  linked_files: [],
19
19
  local_user: nil, # determined at runtime
20
- release_json_path: "%<release_path>/.tomo_release.json",
21
- releases_path: "%<deploy_to>/releases",
22
- revision_log_path: "%<deploy_to>/revisions.log",
23
- shared_path: "%<deploy_to>/shared",
20
+ release_json_path: "%{release_path}/.tomo_release.json",
21
+ releases_path: "%{deploy_to}/releases",
22
+ revision_log_path: "%{deploy_to}/revisions.log",
23
+ shared_path: "%{deploy_to}/shared",
24
24
  tmp_path: "/tmp/tomo",
25
25
  tomo_config_file_path: nil, # determined at runtime
26
26
  run_args: [] # determined at runtime
@@ -7,7 +7,7 @@ module Tomo::Plugin
7
7
  tasks Tomo::Plugin::Env::Tasks
8
8
 
9
9
  defaults bashrc_path: ".bashrc",
10
- env_path: "%<deploy_to>/envrc",
10
+ env_path: "%{deploy_to}/envrc",
11
11
  env_vars: {}
12
12
  end
13
13
  end
@@ -10,7 +10,7 @@ module Tomo::Plugin
10
10
 
11
11
  # rubocop:disable Metrics/LineLength
12
12
  defaults git_branch: "master",
13
- git_repo_path: "%<deploy_to>/git_repo",
13
+ git_repo_path: "%{deploy_to}/git_repo",
14
14
  git_exclusions: [],
15
15
  git_env: { GIT_SSH_COMMAND: "ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no" },
16
16
  git_ref: nil,
@@ -0,0 +1,22 @@
1
+ [Unit]
2
+ Description=Puma HTTP Server for <%= settings[:application] %>
3
+ After=network.target
4
+ Requires=<%= settings[:puma_systemd_socket] %>
5
+ ConditionPathExists=<%= paths.current %>
6
+
7
+ [Service]
8
+ ExecStart=/bin/bash -lc 'exec bundle exec --keep-file-descriptors puma -C config/puma.rb -b tcp://0.0.0.0:<%= settings[:puma_port] %>'
9
+ KillMode=mixed
10
+ Restart=always
11
+ StandardError=syslog
12
+ StandardInput=null
13
+ StandardOutput=syslog
14
+ SyslogIdentifier=%n
15
+ TimeoutStopSec=5
16
+ Type=simple
17
+ WorkingDirectory=<%= paths.current %>
18
+ # Helpful for debugging socket activation, etc.
19
+ # Environment=PUMA_DEBUG=1
20
+
21
+ [Install]
22
+ WantedBy=multi-user.target
@@ -0,0 +1,13 @@
1
+ [Unit]
2
+ Description=Puma HTTP Server Accept Sockets for <%= settings[:application] %>
3
+
4
+ [Socket]
5
+ ListenStream=0.0.0.0:<%= settings[:puma_port] %>
6
+
7
+ # Socket options matching Puma defaults
8
+ NoDelay=true
9
+ ReusePort=true
10
+ Backlog=1024
11
+
12
+ [Install]
13
+ WantedBy=sockets.target
@@ -1,60 +1,121 @@
1
1
  module Tomo::Plugin::Puma
2
2
  class Tasks < Tomo::TaskLibrary
3
+ SystemdUnit = Struct.new(:name, :template, :path)
4
+
5
+ # rubocop:disable Metrics/AbcSize
6
+ def setup_systemd
7
+ linger_must_be_enabled!
8
+
9
+ setup_directories
10
+ remote.write template: socket.template, to: socket.path
11
+ remote.write template: service.template, to: service.path
12
+
13
+ remote.run "systemctl --user daemon-reload"
14
+ remote.run "systemctl", "--user", "enable", service.name, socket.name
15
+ end
16
+ # rubocop:enable Metrics/AbcSize
17
+
18
+ %i[start stop].each do |action|
19
+ define_method(action) do
20
+ remote.run "systemctl", "--user", action, socket.name, service.name
21
+ end
22
+ end
23
+
3
24
  def restart
4
- return if try_restart
25
+ remote.run "systemctl", "--user", "start", socket.name
26
+ remote.run "systemctl", "--user", "restart", service.name
27
+ end
28
+
29
+ def check_active
30
+ logger.info "Checking if puma is active and listening on port #{port}..."
31
+
32
+ active = wait_until { dry_run? || (assert_active! && listening?) }
33
+ remote.run("systemctl", "--user", "status", service.name)
34
+ return if active
35
+
36
+ logger.warn "Timed out waiting for puma to respond on port #{port}"
37
+ end
5
38
 
6
- logger.info "Puma is not running. Starting it now."
7
- start
39
+ def log
40
+ remote.attach "journalctl", "-q",
41
+ raw("--user-unit=#{service.name.shellescape}"),
42
+ *settings[:run_args]
8
43
  end
9
44
 
10
45
  private
11
46
 
12
- def try_restart
13
- ctl_result = remote.chdir(paths.current) do
14
- remote.bundle(
15
- "exec", "pumactl", *control_options, "restart",
16
- raise_on_error: false,
17
- silent: true
18
- )
19
- end
47
+ def port
48
+ settings[:puma_port]
49
+ end
20
50
 
21
- return false if dry_run? || ctl_result.failure?
51
+ def service
52
+ SystemdUnit.new(
53
+ settings[:puma_systemd_service],
54
+ paths.puma_systemd_service_template,
55
+ paths.puma_systemd_service
56
+ )
57
+ end
22
58
 
23
- logger.info(ctl_result.output)
24
- true
59
+ def socket
60
+ SystemdUnit.new(
61
+ settings[:puma_systemd_socket],
62
+ paths.puma_systemd_socket_template,
63
+ paths.puma_systemd_socket
64
+ )
25
65
  end
26
66
 
27
- def start
28
- require_settings :puma_stdout_path, :puma_stderr_path
67
+ def linger_must_be_enabled!
68
+ loginctl_result = remote.run "loginctl", "user-status", remote.host.user
69
+ return unless loginctl_result.stdout.match?(/^\s*Linger:\s*no\s*$/i)
29
70
 
30
- ensure_output_directory
71
+ die <<~ERROR.strip
72
+ Linger must be enabled for the #{remote.host.user} user in order for
73
+ puma to stay running in the background via systemd. Run the following
74
+ command as root:
31
75
 
32
- remote.chdir(paths.current) do
33
- remote.bundle(
34
- "exec", "puma", "--daemon", *control_options, *output_options
35
- )
36
- end
76
+ loginctl enable-linger #{remote.host.user}
77
+ ERROR
37
78
  end
38
79
 
39
- def ensure_output_directory
40
- dirs = [paths.puma_stdout, paths.puma_stderr].map(&:dirname).map(&:to_s)
80
+ def setup_directories
81
+ files = [service.path, socket.path].compact
82
+ dirs = files.map(&:dirname).map(&:to_s)
41
83
  remote.mkdir_p dirs.uniq
42
84
  end
43
85
 
44
- def control_options
45
- require_settings :puma_control_token, :puma_control_url
86
+ def wait_until
87
+ timeout = settings[:puma_check_timeout].to_i
88
+ start = Time.now.to_i
89
+ delay = 1
90
+
91
+ loop do
92
+ sleep delay
93
+ return true if yield
94
+
95
+ elapsed = Time.now.to_i - start
96
+ return false if elapsed >= timeout
97
+
98
+ delay = [delay + 1, timeout - elapsed].min
99
+ end
100
+ end
101
+
102
+ def assert_active!
103
+ return true if remote.run? "systemctl", "--user", "is-active",
104
+ service.name,
105
+ silent: true, raise_on_error: false
106
+
107
+ remote.run "systemctl", "--user", "status", service.name,
108
+ raise_on_error: false
109
+ remote.run "journalctl -q -n 50 --user-unit=#{service.name.shellescape}",
110
+ raise_on_error: false
46
111
 
47
- [
48
- "--control-url", settings[:puma_control_url],
49
- "--control-token", settings[:puma_control_token]
50
- ]
112
+ die "puma failed to start (see previous systemctl and journalctl output)"
51
113
  end
52
114
 
53
- def output_options
54
- options = []
55
- options << ["--redirect-stdout", paths.puma_stdout] if paths.puma_stdout
56
- options << ["--redirect-stderr", paths.puma_stderr] if paths.puma_stderr
57
- options.flatten
115
+ def listening?
116
+ test_url = "http://localhost:#{port}"
117
+ remote.run? "curl -sS --connect-timeout 1 --max-time 10 #{test_url}"\
118
+ " > /dev/null"
58
119
  end
59
120
  end
60
121
  end
@@ -6,9 +6,15 @@ module Tomo::Plugin
6
6
 
7
7
  tasks Tomo::Plugin::Puma::Tasks
8
8
 
9
- defaults puma_control_token: "tomo",
10
- puma_control_url: "tcp://127.0.0.1:9293",
11
- puma_stderr_path: "%<shared_path>/log/puma.err",
12
- puma_stdout_path: "%<shared_path>/log/puma.out"
9
+ # rubocop:disable Metrics/LineLength
10
+ defaults puma_check_timeout: 15,
11
+ puma_port: "3000",
12
+ puma_systemd_service: "puma_%{application}.service",
13
+ puma_systemd_socket: "puma_%{application}.socket",
14
+ puma_systemd_service_path: ".config/systemd/user/%{puma_systemd_service}",
15
+ puma_systemd_socket_path: ".config/systemd/user/%{puma_systemd_socket}",
16
+ puma_systemd_service_template_path: File.expand_path("puma/systemd/service.erb", __dir__),
17
+ puma_systemd_socket_template_path: File.expand_path("puma/systemd/socket.erb", __dir__)
18
+ # rubocop:enable Metrics/LineLength
13
19
  end
14
20
  end
@@ -50,6 +50,7 @@ module Tomo::Plugin::Rails
50
50
  end
51
51
  end
52
52
 
53
+ # TODO: remove
53
54
  def log_tail
54
55
  log_path = raw("#{paths.release.to_s.shellescape}/log/${RAILS_ENV}.log")
55
56
  remote.run("tail", settings[:run_args], log_path)
@@ -7,33 +7,75 @@ module Tomo
7
7
 
8
8
  def initialize(settings)
9
9
  @settings = symbolize(settings)
10
+ @deprecation_warnings = []
10
11
  end
11
12
 
12
13
  def call
13
14
  hash = Hash[settings.keys.map { |name| [name, fetch(name)] }]
14
15
  dump_settings(hash) if Tomo.debug?
16
+ print_deprecation_warnings
15
17
  hash
16
18
  end
17
19
 
18
20
  private
19
21
 
20
- attr_reader :settings
22
+ attr_reader :settings, :deprecation_warnings
21
23
 
24
+ # rubocop:disable Metrics/AbcSize
22
25
  def fetch(name, stack=[])
23
26
  raise_circular_dependency_error(name, stack) if stack.include?(name)
24
27
  value = settings.fetch(name)
25
28
  return value unless value.is_a?(String)
26
29
 
27
- value.gsub(/%<(\w+)>/) do
28
- fetch(Regexp.last_match[1].to_sym, stack + [name])
30
+ value.gsub(/%{(\w+)}|%<(\w+)>/) do
31
+ token = Regexp.last_match[1] || Regexp.last_match[2]
32
+ deprecation_warnings << name if Regexp.last_match[2]
33
+
34
+ fetch(token.to_sym, stack + [name])
29
35
  end
30
36
  end
37
+ # rubocop:enable Metrics/AbcSize
31
38
 
32
39
  def raise_circular_dependency_error(name, stack)
33
40
  dependencies = [*stack, name].join(" -> ")
34
41
  raise "Circular dependency detected in settings: #{dependencies}"
35
42
  end
36
43
 
44
+ # rubocop:disable Metrics/AbcSize
45
+ # rubocop:disable Metrics/MethodLength
46
+ def print_deprecation_warnings
47
+ return if deprecation_warnings.empty?
48
+
49
+ examples = ""
50
+ deprecation_warnings.uniq.each do |name|
51
+ sett = settings[name].inspect
52
+ old_syntax = sett.gsub(
53
+ /%<(\w+)>/,
54
+ Colors.red("%<") + '\1' + Colors.red(">")
55
+ )
56
+ new_syntax = sett.gsub(
57
+ /%<(\w+)>/,
58
+ Colors.green("%{") + '\1' + Colors.green("}")
59
+ )
60
+
61
+ examples << "\n:#{name}\n\n"
62
+ examples << " Replace: set #{name}: #{old_syntax}\n"
63
+ examples << " with this: set #{name}: #{new_syntax}\n"
64
+ end
65
+
66
+ Tomo.logger.warn <<~WARNING
67
+ There are settings using the deprecated %<...> interpolation syntax.
68
+ #{examples}
69
+ #{Colors.red('The %<...> syntax will not work in future versions of tomo.')}
70
+
71
+ WARNING
72
+
73
+ # Make sure people see the warning!
74
+ sleep 5
75
+ end
76
+ # rubocop:enable Metrics/AbcSize
77
+ # rubocop:enable Metrics/MethodLength
78
+
37
79
  def symbolize(hash)
38
80
  hash.each_with_object({}) do |(key, value), symbolized|
39
81
  symbolized[key.to_sym] = value
data/lib/tomo/runtime.rb CHANGED
@@ -84,9 +84,9 @@ module Tomo
84
84
  release = start_time.utc.strftime("%Y%m%d%H%M%S")
85
85
 
86
86
  case type
87
- when :current then "%<current_path>"
88
- when :new then "%<releases_path>/#{release}"
89
- when :tmp then "%<tmp_path>/#{release}"
87
+ when :current then "%{current_path}"
88
+ when :new then "%{releases_path}/#{release}"
89
+ when :tmp then "%{tmp_path}/#{release}"
90
90
  else
91
91
  raise ArgumentError, "release: must be :current, :new, or :tmp"
92
92
  end
@@ -10,7 +10,7 @@ plugin "./plugins/<%= app %>.rb"
10
10
  host "user@hostname.or.ip.address"
11
11
 
12
12
  set application: <%= app.inspect %>
13
- set deploy_to: "/var/www/%<application>"
13
+ set deploy_to: "/var/www/%{application}"
14
14
  set rbenv_ruby_version: <%= RUBY_VERSION.inspect %>
15
15
  set nodenv_node_version: <%= node_version&.inspect || "nil # FIXME" %>
16
16
  set nodenv_yarn_version: <%= yarn_version.inspect %>
@@ -51,6 +51,7 @@ setup do
51
51
  run "rails:db_create"
52
52
  run "rails:db_schema_load"
53
53
  run "rails:db_seed"
54
+ run "puma:setup_systemd"
54
55
  end
55
56
 
56
57
  deploy do
@@ -64,6 +65,7 @@ deploy do
64
65
  run "rails:assets_precompile"
65
66
  run "core:symlink_current"
66
67
  run "puma:restart"
68
+ run "puma:check_active"
67
69
  run "core:clean_releases"
68
70
  run "bundler:clean"
69
71
  run "core:log_revision"
@@ -5,6 +5,10 @@ COPY ./ubuntu_setup.sh ./
5
5
  RUN ./ubuntu_setup.sh
6
6
  COPY ./custom_setup.sh ./
7
7
  RUN ./custom_setup.sh
8
+ COPY ./systemctl.rb /usr/local/bin/systemctl
9
+ RUN chmod a+x /usr/local/bin/systemctl
10
+ COPY ./loginctl.sh /usr/local/bin/loginctl
11
+ RUN chmod a+x /usr/local/bin/loginctl
8
12
  EXPOSE 22
9
13
  EXPOSE 3000
10
14
  CMD ["/usr/sbin/sshd", "-D"]
@@ -9,6 +9,15 @@ at_exit { Tomo::Testing::DockerImage.running_images.each(&:stop) }
9
9
  module Tomo
10
10
  module Testing
11
11
  class DockerImage
12
+ FILES_TO_COPY = %w[
13
+ Dockerfile
14
+ loginctl.sh
15
+ systemctl.rb
16
+ tomo_test_ed25519.pub
17
+ ubuntu_setup.sh
18
+ ].freeze
19
+ private_constant :FILES_TO_COPY
20
+
12
21
  class << self
13
22
  attr_reader :running_images
14
23
  end
@@ -100,8 +109,7 @@ module Tomo
100
109
 
101
110
  def set_up_build_dir
102
111
  FileUtils.mkdir_p(build_dir)
103
- files = %w[Dockerfile tomo_test_ed25519.pub ubuntu_setup.sh]
104
- files.each do |file|
112
+ FILES_TO_COPY.each do |file|
105
113
  FileUtils.cp(File.expand_path(file, __dir__), build_dir)
106
114
  end
107
115
  IO.write(File.join(build_dir, "custom_setup.sh"), setup_script)
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ # THIS FILE IS FOR TESTING PURPOSES ONLY.
4
+ #
5
+ # The real loginctl command does not work in a Docker container, so this empty
6
+ # script takes its place, allowing tomo to work during an E2E test.
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # THIS SCRIPT IS FOR TESTING PURPOSES ONLY.
4
+ #
5
+ # We use Docker to run tomo deploy tests. Docker is not able to run systemd, but
6
+ # tomo needs systemd for starting long-lived processes (e.g. puma). This script
7
+ # simulates the behavior of systemctl commands so that a tomo deploy can succeed
8
+ # in a Docker container where the real systemctl is unavailable.
9
+ #
10
+ # This basic workflow is supported:
11
+ #
12
+ # 1. systemctl --user enable [units...]
13
+ # 2. systemctl --user start [units...]
14
+ # 3. systemctl --user restart [units...]
15
+ # 4. systemctl --user is-active [units...]
16
+ # 5. systemctl --user status [units...]
17
+ #
18
+ # No other commands or options are supported. The only configuration that this
19
+ # script understands is the ExecStart and WorkingDirectory attributes in a
20
+ # *.service file that is expected to be installed in ~/.config/systemd/user/.
21
+ #
22
+ # This script will fork and exec the command listed in ExecStart and store the
23
+ # resulting PID so that it can later be used when stopping or restarting the
24
+ # service. It does not monitor the process, handle stdout/stderr of the process,
25
+ # or do any of the real work that systemd is designed to handle. It simply is
26
+ # the bare minimum behavior needed for tomo deploy to pass an E2E test.
27
+
28
+ require "pstore"
29
+
30
+ COMMANDS = %w[
31
+ daemon-reload
32
+ enable
33
+ is-active
34
+ restart
35
+ start
36
+ status
37
+ stop
38
+ ].freeze
39
+
40
+ def main(args)
41
+ args = args.dup
42
+ raise "First arg must be --user" unless args.shift == "--user"
43
+ raise "Missing command" if args.empty?
44
+
45
+ command = args.shift
46
+ raise "Unknown command: #{command}" unless COMMANDS.include?(command)
47
+
48
+ run(command, args)
49
+ end
50
+
51
+ def run(command, args)
52
+ return daemon_reload(args) if command == "daemon-reload"
53
+ raise "#{command} requires an argument" if args.empty?
54
+
55
+ args.each { |name| Unit.find(name).public_send(command.tr("-", "_")) }
56
+ end
57
+
58
+ def daemon_reload(args)
59
+ raise "daemon-reload does not accept arguments" unless args.empty?
60
+ end
61
+
62
+ class Unit
63
+ def self.find(name)
64
+ path = File.join(File.expand_path("~/.config/systemd/user/"), name)
65
+ raise "Unknown unit: #{name}" unless File.file?(path)
66
+ return Service.new(name, IO.read(path)) if name.end_with?(".service")
67
+
68
+ new(name, IO.read(path))
69
+ end
70
+
71
+ def initialize(name, spec)
72
+ @name = name
73
+ @spec = spec
74
+ end
75
+
76
+ def enable
77
+ with_persistent_state { |state| state[:enabled] = true }
78
+ end
79
+
80
+ def status
81
+ puts "● #{name}"
82
+ puts " Loaded: loaded (enabled; vendor preset: enabled)" if enabled?
83
+ end
84
+
85
+ def start
86
+ must_be_enabled!
87
+ end
88
+
89
+ def stop
90
+ must_be_enabled!
91
+ end
92
+
93
+ def restart
94
+ must_be_enabled!
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :name, :spec
100
+
101
+ def must_be_enabled!
102
+ raise "#{name} must be enabled first" unless enabled?
103
+ end
104
+
105
+ def enabled?
106
+ with_persistent_state { |state| state[:enabled] }
107
+ end
108
+
109
+ def with_persistent_state
110
+ @pstore ||= begin
111
+ pstore_path = File.expand_path("~/.config/systemd/state.db")
112
+ PStore.new(pstore_path)
113
+ end
114
+
115
+ @pstore.transaction do
116
+ state = @pstore[name] ||= {}
117
+ yield(state)
118
+ end
119
+ end
120
+ end
121
+
122
+ class Service < Unit
123
+ def is_active # rubocop:disable Naming/PredicateName
124
+ exit(false) unless started?
125
+ puts "active"
126
+ end
127
+
128
+ def start
129
+ super
130
+ raise "#{name} is already running" if started?
131
+
132
+ working_dir, executable = parse
133
+
134
+ if (pid = Process.fork)
135
+ with_persistent_state { |state| state[:pid] = pid }
136
+ Process.detach(pid)
137
+ return
138
+ end
139
+
140
+ with_detached_io { Dir.chdir(working_dir) { Process.exec(executable) } }
141
+ end
142
+
143
+ def stop
144
+ with_persistent_state do |state|
145
+ pid = state.delete(:pid)
146
+ Process.kill("TERM", pid) unless pid.nil?
147
+ end
148
+ end
149
+
150
+ def restart
151
+ super
152
+ stop if started?
153
+ start
154
+ end
155
+
156
+ def status
157
+ super
158
+ puts " Active: active (running)" if started?
159
+ end
160
+
161
+ private
162
+
163
+ def started?
164
+ with_persistent_state { |state| !state[:pid].nil? }
165
+ end
166
+
167
+ def parse
168
+ config = Hash[spec.scan(/^([^\s=]+)=\s*(\S.*?)\s*$/)]
169
+ working_dir = config["WorkingDirectory"] || File.expand_path("~")
170
+ executable = config.fetch("ExecStart") do
171
+ raise "#{name} is missing ExecStart attribute"
172
+ end
173
+
174
+ [working_dir, executable]
175
+ end
176
+
177
+ def with_detached_io
178
+ null_in = File.open(File::NULL, "r")
179
+ null_out = File.open(File::NULL, "w")
180
+ $stdin.reopen(null_in)
181
+ $stderr.reopen(null_out)
182
+ $stdout.reopen(null_out)
183
+ yield
184
+ end
185
+ end
186
+
187
+ main(ARGV) if $PROGRAM_NAME == __FILE__
data/lib/tomo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Tomo
2
- VERSION = "0.8.1".freeze
2
+ VERSION = "0.9.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tomo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-29 00:00:00.000000000 Z
11
+ date: 2019-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,42 +100,56 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '12.3'
103
+ version: '13.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '12.3'
110
+ version: '13.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rubocop
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - '='
116
116
  - !ruby/object:Gem::Version
117
- version: 0.74.0
117
+ version: 0.75.1
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - '='
123
123
  - !ruby/object:Gem::Version
124
- version: 0.74.0
124
+ version: 0.75.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-minitest
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '='
130
+ - !ruby/object:Gem::Version
131
+ version: 0.3.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '='
137
+ - !ruby/object:Gem::Version
138
+ version: 0.3.0
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: rubocop-performance
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
143
  - - '='
130
144
  - !ruby/object:Gem::Version
131
- version: 1.4.1
145
+ version: 1.5.0
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
150
  - - '='
137
151
  - !ruby/object:Gem::Version
138
- version: 1.4.1
152
+ version: 1.5.0
139
153
  description:
140
154
  email:
141
155
  - opensource@mattbrictson.com
@@ -221,6 +235,8 @@ files:
221
235
  - lib/tomo/plugin/nodenv.rb
222
236
  - lib/tomo/plugin/nodenv/tasks.rb
223
237
  - lib/tomo/plugin/puma.rb
238
+ - lib/tomo/plugin/puma/systemd/service.erb
239
+ - lib/tomo/plugin/puma/systemd/socket.erb
224
240
  - lib/tomo/plugin/puma/tasks.rb
225
241
  - lib/tomo/plugin/rails.rb
226
242
  - lib/tomo/plugin/rails/helpers.rb
@@ -274,12 +290,14 @@ files:
274
290
  - lib/tomo/testing/host_extensions.rb
275
291
  - lib/tomo/testing/local.rb
276
292
  - lib/tomo/testing/log_capturing.rb
293
+ - lib/tomo/testing/loginctl.sh
277
294
  - lib/tomo/testing/mock_plugin_tester.rb
278
295
  - lib/tomo/testing/mocked_exec_error.rb
279
296
  - lib/tomo/testing/mocked_exit_error.rb
280
297
  - lib/tomo/testing/plugin_tester.rb
281
298
  - lib/tomo/testing/remote_extensions.rb
282
299
  - lib/tomo/testing/ssh_extensions.rb
300
+ - lib/tomo/testing/systemctl.rb
283
301
  - lib/tomo/testing/tomo_test_ed25519
284
302
  - lib/tomo/testing/tomo_test_ed25519.pub
285
303
  - lib/tomo/testing/ubuntu_setup.sh